[
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: ['https://tailwindcss.com/sponsor']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug report\nabout: If you've already asked for help with a problem and confirmed something is broken with prettier-plugin-tailwindcss itself, create a bug report.\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n<!-- 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. -->\n\n**What version of `prettier-plugin-tailwindcss` are you using?**\n\nFor example: v0.1.7\n\n**What version of Tailwind CSS are you using?**\n\nFor example: v3.0.22\n\n**What version of Node.js are you using?**\n\nFor example: v12.0.0\n\n**What package manager are you using?**\n\nFor example: npm, Yarn\n\n**What operating system are you using?**\n\nFor example: macOS, Windows\n\n**Reproduction URL**\n\nA 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.\n\n**Describe your issue**\n\nDescribe the problem you're seeing, any important steps to reproduce and what behavior you expect instead\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Get Help\n    url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=help&title=%5BPrettier%20Plugin%5D%20\n    about: If you can't get something to work the way you expect, open a question in our discussion forums.\n  - name: Feature Request\n    url: https://github.com/tailwindlabs/tailwindcss/discussions/new?category=ideas&title=%5BPrettier%20Plugin%5D%20\n    about: 'Suggest any ideas you have using our discussion forums.'\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\npermissions:\n  contents: read\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\nenv:\n  CI: true\n  NODE_VERSION: 22\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Use Node.js ${{ env.NODE_VERSION }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Check formatting\n        run: pnpm run format --check\n\n      - name: Lint\n        run: pnpm run lint\n\n      - name: Build Prettier Plugin\n        run: pnpm run build\n\n      - name: Test against Prettier v3\n        run: pnpm run test\n"
  },
  {
    "path": ".github/workflows/prepare-release.yml",
    "content": "name: Prepare Release\n\non:\n  workflow_dispatch:\n  push:\n    tags:\n      - 'v*'\n\nenv:\n  CI: true\n  NODE_VERSION: 22\n\npermissions:\n  contents: read\n\njobs:\n  build:\n    permissions:\n      contents: write # for softprops/action-gh-release to create GitHub release\n\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - run: git fetch --tags -f\n\n      - name: Resolve version\n        id: vars\n        run: |\n          echo \"TAG_NAME=$(git describe --tags --abbrev=0)\" >> $GITHUB_ENV\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Use Node.js ${{ env.NODE_VERSION }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          registry-url: 'https://registry.npmjs.org'\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Get release notes\n        run: |\n          RELEASE_NOTES=$(pnpm run release-notes --silent)\n          echo \"RELEASE_NOTES<<EOF\" >> $GITHUB_ENV\n          echo \"$RELEASE_NOTES\" >> $GITHUB_ENV\n          echo \"EOF\" >> $GITHUB_ENV\n\n      - name: Release\n        uses: softprops/action-gh-release@v2\n        with:\n          draft: true\n          tag_name: ${{ env.TAG_NAME }}\n          body: |\n            ${{ env.RELEASE_NOTES }}\n"
  },
  {
    "path": ".github/workflows/release-insiders.yml",
    "content": "name: Release Insiders\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  id-token: write\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\nenv:\n  CI: true\n  NODE_VERSION: 22\n  RELEASE_CHANNEL: insiders\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Use Node.js ${{ env.NODE_VERSION }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Build Prettier Plugin\n        run: pnpm run build\n\n      - name: Test\n        run: pnpm run test\n\n      - name: Resolve version\n        id: vars\n        run: |\n          echo \"SHA_SHORT=$(git rev-parse --short HEAD)\" >> $GITHUB_ENV\n\n      - name: \"Version based on commit: 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }}\"\n        run: pnpm version 0.0.0-${{ env.RELEASE_CHANNEL }}.${{ env.SHA_SHORT }} --force --no-git-tag-version\n\n      - name: Publish\n        run: pnpm publish --provenance --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  release:\n    types: [published]\n\npermissions:\n  contents: read\n  id-token: write\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}\n  cancel-in-progress: true\n\nenv:\n  CI: true\n  NODE_VERSION: 22\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Use Node.js ${{ env.NODE_VERSION }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ env.NODE_VERSION }}\n          cache: pnpm\n          registry-url: \"https://registry.npmjs.org\"\n\n      - name: Install dependencies\n        run: pnpm install\n\n      - name: Build Prettier Plugin\n        run: pnpm run build\n\n      - name: Test\n        run: pnpm run test\n\n      - name: Calculate environment variables\n        run: |\n          echo \"RELEASE_CHANNEL=$(pnpm run release-channel --silent)\" >> $GITHUB_ENV\n\n      - name: Publish\n        run: pnpm publish --provenance --tag ${{ env.RELEASE_CHANNEL }} --no-git-checks\n        env:\n          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n/dist\n"
  },
  {
    "path": ".oxlintrc.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json\",\n  \"rules\": {\n    \"typescript/unbound-method\": \"off\"\n  }\n}\n"
  },
  {
    "path": ".prettierignore",
    "content": "tests/fixtures\n.github/\ndist/\nREADME.md\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]\n\n### Changed\n\n- Remove top-level await ([#420](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/420))\n- Improve load-time performance ([#420](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/420))\n\n### Fixed\n\n- Collapse whitespace in template literals with adjacent quasis ([#427](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/427))\n\n## [0.7.2] - 2025-12-01\n\n### Fixed\n\n- Load compatible plugins sequentially to work around race conditions in Node.js ([#412](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/412))\n- Fix compatibility with `prettier-plugin-svelte` when using Prettier v3.7+ ([#418](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/418))\n\n## [0.7.1] - 2025-10-17\n\n### Fixed\n\n- Match against correct name of dynamic attributes when using regexes ([#410](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/410))\n\n## [0.7.0] - 2025-10-14\n\n### Added\n\n- Format quotes in `@source`, `@plugin`, and `@config` ([#387](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/387))\n- Sort in function calls in Twig ([#358](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/358))\n- Sort in callable template literals ([#367](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/367))\n- Sort in function calls mixed with property accesses ([#367](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/367))\n- Support regular expression patterns for attributes ([#405](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/405))\n- Support regular expression patterns for function names ([#405](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/405))\n\n### Changed\n- 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))\n- 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))\n- Fallback to Tailwind CSS v4 instead of v3 by default ([#390](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/390))\n- Don't augment global Prettier `ParserOptions` and `RequiredOptions` types ([#354](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/354))\n- Drop support for `prettier-plugin-import-sort` ([#385](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/385))\n\n### Fixed\n- Handle quote escapes in LESS when sorting `@apply` ([#392](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/392))\n- Fix whitespace removal inside nested concat and template expressions ([#396](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/396))\n\n## [0.6.14] - 2025-07-09\n\n- 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))\n- Sort template literals in Angular expressions ([#377](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/377))\n- Don't repeatedly add backslashes to escape sequences when formatting ([#381](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/381))\n\n## [0.6.13] - 2025-06-19\n\n- Prevent Svelte files from breaking when there are duplicate classes ([#359](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/359))\n- 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))\n\n## [0.6.12] - 2025-05-30\n\n- Add internal (unsupported) option to load Tailwind CSS using a different package name ([#366](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/366))\n\n## [0.6.11] - 2025-01-23\n\n- Support TypeScript configs and plugins when using v4 ([#342](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/342))\n\n## [0.6.10] - 2025-01-15\n\n- Add support for `@zackad/prettier-plugin-twig` ([#327](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/327))\n- Drop support for `@zackad/prettier-plugin-twig-melody` ([#327](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/327))\n- Update Prettier options types ([#325](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/325))\n- Don't remove whitespace inside template literals in Svelte ([#332](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/332))\n\n## [0.6.9] - 2024-11-19\n\n- Introduce `tailwindStylesheet` option to replace `tailwindEntryPoint` ([#330](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/330))\n\n## [0.6.8] - 2024-09-24\n\n- Fix crash ([#320](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/320))\n\n## [0.6.7] - 2024-09-24\n\n- Improved performance with large Svelte, Liquid, and Angular files ([#312](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/312))\n- Add support for `@plugin` and `@config` in v4 ([#316](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/316))\n- Add support for Tailwind CSS v4.0.0-alpha.25 ([#317](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/317))\n\n## [0.6.6] - 2024-08-09\n\n- Add support for `prettier-plugin-multiline-arrays` ([#299](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/299))\n- Add resolution cache for known plugins ([#301](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/301))\n- Support Tailwind CSS `v4.0.0-alpha.19` ([#310](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/310))\n\n## [0.6.5] - 2024-06-17\n\n- Only re-apply string escaping when necessary ([#295](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/295))\n\n## [0.6.4] - 2024-06-12\n\n- Export `PluginOptions` type ([#292](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/292))\n\n## [0.6.3] - 2024-06-11\n\n- Improve detection of string concatenation ([#288](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/288))\n\n## [0.6.2] - 2024-06-07\n\n### Changed\n\n- Only remove duplicate Tailwind classes ([#277](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/277))\n- Make sure escapes in classes are preserved in string literals ([#286](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/286))\n\n## [0.6.1] - 2024-05-31\n\n### Added\n\n- Add new `tailwindPreserveDuplicates` option to disable removal of duplicate classes ([#276](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/276))\n\n### Fixed\n\n- Improve handling of whitespace removal when concatenating strings ([#276](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/276))\n- Fix a bug where Angular expressions may produce invalid code after sorting ([#276](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/276))\n- Disabled whitespace and duplicate class removal for Liquid and Svelte ([#276](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/276))\n\n## [0.6.0] - 2024-05-30\n\n### Changed\n\n- Remove duplicate classes ([#272](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/272))\n- Remove extra whitespace around classes ([#272](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/272))\n\n## [0.5.14] - 2024-04-15\n\n### Fixed\n\n- Fix detection of v4 projects on Windows ([#265](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/265))\n\n## [0.5.13] - 2024-03-27\n\n### Added\n\n- Add support for `@zackad/prettier-plugin-twig-melody` ([#255](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/255))\n\n## [0.5.12] - 2024-03-06\n\n### Added\n\n- Add support for `prettier-plugin-sort-imports` ([#241](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/241))\n- Add support for Tailwind CSS v4.0 ([#249](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/249))\n\n## [0.5.11] - 2024-01-05\n\n### Changed\n\n- Bumped bundled version of Tailwind CSS to v3.4.1 ([#240](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/240))\n\n## [0.5.10] - 2023-12-28\n\n### Changed\n\n- Bumped bundled version of Tailwind CSS to v3.4 ([#235](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/235))\n\n## [0.5.9] - 2023-12-05\n\n### Fixed\n\n- Fixed location of embedded preflight CSS file ([#231](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/231))\n\n## [0.5.8] - 2023-12-05\n\n### Added\n\n- Re-enable support for `prettier-plugin-marko` ([#229](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/229))\n\n## [0.5.7] - 2023-11-08\n\n### Fixed\n\n- Fix sorting inside dynamic custom attributes ([#225](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/225))\n\n## [0.5.6] - 2023-10-12\n\n### Fixed\n\n- Fix sorting inside `{{ … }}` expressions when using `@shopify/prettier-plugin-liquid` v1.3+ ([#222](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/222))\n\n## [0.5.5] - 2023-10-03\n\n### Fixed\n\n- Sort classes inside `className` in Astro ([#215](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/215))\n- Support member access on function calls ([#218](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/218))\n\n## [0.5.4] - 2023-08-31\n\n### Fixed\n\n- Type `tailwindFunctions` and `tailwindAttributes` as optional ([#206](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/206))\n- Don’t break `@apply … #{'!important'}` sorting in SCSS ([#212](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/212))\n\n## [0.5.3] - 2023-08-15\n\n### Fixed\n\n- Fix CJS `__dirname` interop on Windows ([#204](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/204))\n\n## [0.5.2] - 2023-08-11\n\n### Fixed\n\n- Fix intertop with bundled CJS dependencies ([#199](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/199))\n\n## [0.5.1] - 2023-08-10\n\n### Fixed\n\n- Updated Prettier peer dependency ([#197](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/197))\n\n## [0.5.0] - 2023-08-10\n\n### Added\n\n- Sort expressions in Astro's `class:list` attribute ([#192](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/192))\n- Re-enabled support for plugins when using Prettier v3+ ([#195](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/195))\n\n## [0.4.1] - 2023-07-14\n\n### Fixed\n\n- Don't move partial classes inside Twig attributes ([#184](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/184))\n\n## [0.4.0] - 2023-07-11\n\n### Added\n\n- Export types for Prettier config ([#162](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/162))\n- Add Prettier v3 support ([#179](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/179))\n\n### Fixed\n\n- Don't move partial classes inside Liquid script attributes ([#164](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/164))\n- Do not split classes by non-ASCII whitespace ([#166](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/166))\n- Match tagged template literals with tag expressions ([#169](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/169))\n\n## [0.3.0] - 2023-05-15\n\n### Added\n\n- Added support for `prettier-plugin-marko` ([#151](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/151))\n- Allow sorting of custom attributes, functions, and tagged template literals ([#155](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/155))\n\n### Fixed\n\n- Speed up formatting ([#153](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/153))\n- Fix plugin compatibility when loaded with require ([#159](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/159))\n\n## [0.2.8] - 2023-04-28\n\n### Changed\n\n- Remove support for `@prettier/plugin-php` ([#152](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/152))\n\n## [0.2.7] - 2023-04-05\n\n### Fixed\n\n- Don't break liquid tags inside attributes when sorting classes ([#143](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/143))\n\n## [0.2.6] - 2023-03-29\n\n### Added\n\n- Support ESM and TS config files ([#137](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/137))\n\n### Fixed\n\n- Load `tailwindcss` modules from nearest instance only ([#139](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/139))\n\n## [0.2.5] - 2023-03-17\n\n### Fixed\n\n- Fix class sorting in `capture` liquid tag ([#131](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/131))\n\n## [0.2.4] - 2023-03-02\n\n### Fixed\n\n- Sort `class` attribute on components and custom elements in Astro ([#129](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/129))\n\n## [0.2.3] - 2023-02-15\n\n### Fixed\n\n- Don't sort classes in Glimmer `concat` helper ([#119](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/119))\n- Add support for `@ianvs/prettier-plugin-sort-imports` ([#122](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/122))\n\n## [0.2.2] - 2023-01-24\n\n### Fixed\n\n- Add prettier plugins to peer dependencies ([#114](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/114))\n- Traverse await expression blocks in Svelte ([#118](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/118))\n\n## [0.2.1] - 2022-12-08\n\n### Fixed\n\n- Fix support for latest Shopify Liquid plugin ([#109](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/109))\n\n## [0.2.0] - 2022-11-25\n\n### Changed\n\n- Don't bundle `prettier-plugin-svelte` ([#101](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/101))\n\n### Added\n\n- 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))\n\n## [0.1.13] - 2022-07-25\n\n### Fixed\n\n- Fix error when using Angular pipes ([#86](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/86))\n\n## [0.1.12] - 2022-07-07\n\n### Added\n\n- Add support for Glimmer / Handlebars ([#83](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/83))\n\n## [0.1.11] - 2022-05-16\n\n### Changed\n\n- Update `prettier-plugin-svelte` to `v2.7.0` ([#77](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/77))\n\n### Fixed\n\n- Fix sorting in Svelte `:else` blocks ([#79](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/79))\n\n## [0.1.10] - 2022-04-20\n\n### Removed\n\n- 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))\n\n## [0.1.9] - 2022-04-19\n\n### Added\n\n- Add license file ([#64](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/64))\n- Add whitespace tidying and duplicate class removal ([#70](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/70))\n\n## [0.1.8] - 2022-02-24\n\n### Changed\n\n- Use Tailwind's `getClassOrder` API when available ([#57](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/57))\n\n### Fixed\n\n- Fix Tailwind config file resolution when Prettier config file is not present ([#62](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/62))\n\n## [0.1.7] - 2022-02-09\n\n### Fixed\n\n- Fix single quotes being converted to double quotes ([#51](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/51))\n\n## [0.1.6] - 2022-02-08\n\n### Fixed\n\n- Fix error when no Prettier options provided ([#46](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/46))\n\n## [0.1.5] - 2022-02-04\n\n### Added\n\n- Add support for MDX ([#30](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/30))\n\n### Fixed\n\n- Fix error when formatting Svelte files that contain `let:class` attributes ([#24](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/24))\n\n## [0.1.4] - 2022-01-25\n\n### Fixed\n\n- Handle empty class attributes ([#17](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/17))\n- Handle TypeScript syntax in Vue/Angular class attributes ([#18](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/18))\n\n## [0.1.3] - 2022-01-24\n\n### Fixed\n\n- Ignore `!important` when sorting `@apply` classes ([#4](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/4))\n\n## [0.1.2] - 2022-01-24\n\n### Fixed\n\n- Fix error when using nullish coalescing operator in Vue/Angular ([#2](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/2))\n\n[unreleased]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.7.2...HEAD\n[0.7.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.7.1...v0.7.2\n[0.7.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.7.0...v0.7.1\n[0.7.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.14...v0.7.0\n[0.6.14]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.13...v0.6.14\n[0.6.13]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.12...v0.6.13\n[0.6.12]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.11...v0.6.12\n[0.6.11]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.10...v0.6.11\n[0.6.10]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.9...v0.6.10\n[0.6.9]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.8...v0.6.9\n[0.6.8]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.7...v0.6.8\n[0.6.7]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.6...v0.6.7\n[0.6.6]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.5...v0.6.6\n[0.6.5]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.4...v0.6.5\n[0.6.4]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.3...v0.6.4\n[0.6.3]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.2...v0.6.3\n[0.6.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.1...v0.6.2\n[0.6.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.6.0...v0.6.1\n[0.6.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.14...v0.6.0\n[0.5.14]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.13...v0.5.14\n[0.5.13]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.12...v0.5.13\n[0.5.12]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.11...v0.5.12\n[0.5.11]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.10...v0.5.11\n[0.5.10]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.9...v0.5.10\n[0.5.9]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.8...v0.5.9\n[0.5.8]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.7...v0.5.8\n[0.5.7]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.6...v0.5.7\n[0.5.6]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.5...v0.5.6\n[0.5.5]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.4...v0.5.5\n[0.5.4]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.3...v0.5.4\n[0.5.3]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.2...v0.5.3\n[0.5.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.1...v0.5.2\n[0.5.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.5.0...v0.5.1\n[0.5.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.4.1...v0.5.0\n[0.4.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.4.0...v0.4.1\n[0.4.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.3.0...v0.4.0\n[0.3.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.8...v0.3.0\n[0.2.8]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.7...v0.2.8\n[0.2.7]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.6...v0.2.7\n[0.2.6]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.5...v0.2.6\n[0.2.5]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.4...v0.2.5\n[0.2.4]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.3...v0.2.4\n[0.2.3]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.2...v0.2.3\n[0.2.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.1...v0.2.2\n[0.2.1]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.2.0...v0.2.1\n[0.2.0]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.13...v0.2.0\n[0.1.13]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.12...v0.1.13\n[0.1.12]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.11...v0.1.12\n[0.1.11]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.10...v0.1.11\n[0.1.10]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.9...v0.1.10\n[0.1.9]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.8...v0.1.9\n[0.1.8]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.7...v0.1.8\n[0.1.7]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.6...v0.1.7\n[0.1.6]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.5...v0.1.6\n[0.1.5]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.4...v0.1.5\n[0.1.4]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.3...v0.1.4\n[0.1.3]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/v0.1.2...v0.1.3\n[0.1.2]: https://github.com/tailwindlabs/prettier-plugin-tailwindcss/compare/d9c27f07a69bf9feec7f9d889426ad2ba76e1b09...v0.1.2\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) Tailwind Labs Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"https://raw.githubusercontent.com/tailwindlabs/prettier-plugin-tailwindcss/main/.github/banner.jpg\" alt=\"prettier-plugin-tailwindcss\" />\n\nA [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).\n\n## Installation\n\nTo get started, install `prettier-plugin-tailwindcss` as a dev-dependency:\n\n```sh\nnpm install -D prettier prettier-plugin-tailwindcss\n```\n\nThen add the plugin to your [Prettier configuration](https://prettier.io/docs/en/configuration.html):\n\n```json5\n// .prettierrc\n{\n  \"plugins\": [\"prettier-plugin-tailwindcss\"]\n}\n```\n\nWhen using a JavaScript config, you can import the types for IntelliSense:\n\n```js\n// prettier.config.js\n\n/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */\nexport default {\n  plugins: [\"prettier-plugin-tailwindcss\"],\n}\n```\n\n## Upgrading to v0.5.x\n\nAs 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).\n\n## Options\n\n### Specifying your Tailwind stylesheet path (Tailwind CSS v4+)\n\nWhen 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.\n\nNote that paths are resolved relative to the Prettier configuration file.\n\n```json5\n// .prettierrc\n{\n  \"tailwindStylesheet\": \"./resources/css/app.css\"\n}\n```\n\n### Specifying your Tailwind JavaScript config path (Tailwind CSS v3)\n\nTo 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`).\n\nBy 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.\n\nNote that paths are resolved relative to the Prettier configuration file.\n\n```json5\n// .prettierrc\n{\n  \"tailwindConfig\": \"./styles/tailwind.config.js\"\n}\n```\n\nIf a local configuration file cannot be found the plugin will fallback to the default Tailwind configuration.\n\n## Sorting non-standard attributes\n\nBy default this plugin sorts classes in the `class` attribute, any framework-specific equivalents like `className`, `:class`, `[ngClass]`, and any Tailwind `@apply` directives.\n\nYou can sort additional attributes using the `tailwindAttributes` option, which takes an array of attribute names:\n\n```json5\n// .prettierrc\n{\n  \"tailwindAttributes\": [\"myClassList\"]\n}\n```\n\nWith this configuration, any classes found in the `myClassList` attribute will be sorted:\n\n```jsx\nfunction MyButton({ children }) {\n  return (\n    <button myClassList=\"rounded bg-blue-500 px-4 py-2 text-base text-white\">\n      {children}\n    </button>\n  );\n}\n```\n\n### Using regex patterns\n\nYou 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:\n\n```json5\n// .prettierrc\n{\n  \"tailwindAttributes\": [\"myClassList\", \"/data-.*/\"]\n}\n```\n\nThis example will sort classes in the `myClassList` attribute as well as any attribute starting with `data-`:\n\n```jsx\nfunction MyButton({ children }) {\n  return (\n    <button\n      myClassList=\"rounded bg-blue-500 px-4 py-2 text-base text-white\"\n      data-theme=\"dark:bg-gray-800 bg-white\"\n      data-classes=\"flex items-center\"\n    >\n      {children}\n    </button>\n  );\n}\n```\n\n## Sorting classes in function calls\n\nIn 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/).\n\nYou can sort classes in function calls using the `tailwindFunctions` option, which takes a list of function names:\n\n```json5\n// .prettierrc\n{\n  \"tailwindFunctions\": [\"clsx\"]\n}\n```\n\nWith this configuration, any classes in `clsx()` function calls will be sorted:\n\n```jsx\nimport clsx from 'clsx'\n\nfunction MyButton({ isHovering, children }) {\n  let classes = clsx(\n    'rounded bg-blue-500 px-4 py-2 text-base text-white',\n    {\n      'bg-blue-700 text-gray-100': isHovering,\n    },\n  )\n\n  return (\n    <button className={classes}>\n      {children}\n    </button>\n  )\n}\n```\n\n## Sorting classes in template literals\n\nThis plugin also enables sorting of classes in tagged template literals.\n\nYou can sort classes in template literals using the `tailwindFunctions` option, which takes a list of function names:\n\n```json5\n// .prettierrc\n{\n  \"tailwindFunctions\": [\"tw\"],\n}\n```\n\nWith this configuration, any classes in template literals tagged with `tw` will automatically be sorted:\n\n```jsx\nimport { View, Text } from 'react-native'\nimport tw from 'twrnc'\n\nfunction MyScreen() {\n  return (\n    <View style={tw`bg-white p-4 dark:bg-black`}>\n      <Text style={tw`text-md text-black dark:text-white`}>Hello World</Text>\n    </View>\n  )\n}\n```\n\nThis feature can be used with third-party libraries like `twrnc` or you can create your own tagged template by defining this \"identity\" function:\n\n```js\nconst tw = (strings, ...values) => String.raw({ raw: strings }, ...values)\n```\n\nOnce added, tag your strings with the function and the plugin will sort them:\n\n```js\nconst mySortedClasses = tw`bg-white p-4 dark:bg-black`\n```\n\n## Public API\n\nIf you want to use the Tailwind class sorting logic outside of Prettier, import from the\n`sorter` entrypoint:\n\n```js\nimport { createSorter } from 'prettier-plugin-tailwindcss/sorter'\n\nlet sorter = await createSorter({\n  base: '/path/to/project',\n  stylesheetPath: './app.css',\n})\n\n// Sort HTML class attributes (space-separated strings)\nlet sorted = sorter.sortClassAttributes([\n  'sm:bg-tomato bg-red-500',\n  'p-4 m-2'\n])\n// Returns: ['bg-red-500 sm:bg-tomato', 'm-2 p-4']\n\n// Sort class lists (arrays of class names)\nlet sortedLists = sorter.sortClassLists([\n  ['sm:bg-tomato', 'bg-red-500'],\n  ['p-4', 'm-2']\n])\n// Returns: [['bg-red-500', 'sm:bg-tomato'], ['m-2', 'p-4']]\n```\n\n### API Options\n\nThe `createSorter` function accepts the following options:\n\n- **`base`** (optional): The directory used to resolve relative file paths. Defaults to the current working directory.\n- **`filepath`** (optional): The path to the file being formatted. When provided, Tailwind CSS is resolved relative to this path.\n- **`configPath`** (optional): Path to the Tailwind CSS config file (v3). Paths are resolved relative to `base`.\n- **`stylesheetPath`** (optional): Path to the CSS stylesheet used by Tailwind CSS (v4+). Paths are resolved relative to `base`.\n- **`preserveWhitespace`** (optional): Whether to preserve whitespace around classes. Default: `false`.\n- **`preserveDuplicates`** (optional): Whether to preserve duplicate classes. Default: `false`.\n\n### Sorter Methods\n\nThe sorter object returned by `createSorter` has two methods:\n\n- **`sortClassAttributes(classes: string[]): string[]`**\n  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).\n\n- **`sortClassLists(classes: string[][]): (string | null)[][]`**\n  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.\n\n### Using regex patterns\n\nLike 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.\n\n## Preserving whitespace\n\nThis plugin automatically removes unnecessary whitespace between classes to ensure consistent formatting. If you prefer to preserve whitespace, you can use the `tailwindPreserveWhitespace` option:\n\n```json5\n// .prettierrc\n{\n  \"tailwindPreserveWhitespace\": true,\n}\n```\n\nWith this configuration, any whitespace surrounding classes will be preserved:\n\n```jsx\nimport clsx from 'clsx'\n\nfunction MyButton({ isHovering, children }) {\n  return (\n    <button className=\" rounded  bg-blue-500 px-4  py-2     text-base text-white \">\n      {children}\n    </button>\n  )\n}\n```\n\n## Preserving duplicate classes\n\nThis 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.\n\nIf removing duplicate classes is causing issues in your project, you can use the `tailwindPreserveDuplicates` option to disable this behavior:\n\n```json5\n// .prettierrc\n{\n  \"tailwindPreserveDuplicates\": true,\n}\n```\n\nWith this configuration, anything we perceive as duplicate classes will be preserved:\n\n```html\n<div\n  class=\"\n    {f:if(condition: isCompact, then: 'grid-cols-3', else: 'grid-cols-5')}\n    {f:if(condition: isDark, then: 'bg-black/50', else: 'bg-white/50')}\n    grid gap-4 p-4\n  \"\n>\n</div>\n```\n\n## Compatibility with other Prettier plugins\n\nThis 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:\n\n- `@ianvs/prettier-plugin-sort-imports`\n- `@prettier/plugin-pug`\n- `@shopify/prettier-plugin-liquid`\n- `@trivago/prettier-plugin-sort-imports`\n- `prettier-plugin-astro`\n- `prettier-plugin-css-order`\n- `prettier-plugin-jsdoc`\n- `prettier-plugin-multiline-arrays`\n- `prettier-plugin-organize-attributes`\n- `prettier-plugin-organize-imports`\n- `prettier-plugin-svelte`\n- `prettier-plugin-sort-imports`\n\nOne limitation with this approach is that `prettier-plugin-tailwindcss` *must* be loaded last.\n\n```json5\n// .prettierrc\n{\n  // ..\n  \"plugins\": [\n    \"prettier-plugin-svelte\",\n    \"prettier-plugin-organize-imports\",\n    \"prettier-plugin-tailwindcss\" // MUST come last\n  ]\n}\n```\n"
  },
  {
    "path": "knip.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/knip@5/schema.json\",\n  \"entry\": [\"scripts/*.js\"],\n  \"project\": [\"src/**/*.ts\", \"scripts/**/*.js\", \"tests/**/*.ts\"],\n  \"ignore\": [\"src/**/*.d.ts\", \"tests/fixtures/**\"],\n  \"ignoreDependencies\": [\n    \"@ianvs/prettier-plugin-sort-imports\",\n    \"@prettier/plugin-oxc\",\n    \"@prettier/plugin-hermes\",\n    \"@prettier/plugin-pug\",\n    \"@shopify/prettier-plugin-liquid\",\n    \"@trivago/prettier-plugin-sort-imports\",\n    \"@zackad/prettier-plugin-twig\",\n    \"prettier-plugin-astro\",\n    \"prettier-plugin-css-order\",\n    \"prettier-plugin-jsdoc\",\n    \"prettier-plugin-marko\",\n    \"prettier-plugin-multiline-arrays\",\n    \"prettier-plugin-organize-attributes\",\n    \"prettier-plugin-organize-imports\",\n    \"prettier-plugin-sort-imports\",\n    \"prettier-plugin-svelte\",\n    \"import-sort-style-module\",\n    \"marko\",\n    \"postcss\",\n    \"postcss-import\"\n  ],\n  \"rules\": {\n    \"exports\": \"off\",\n    \"types\": \"off\"\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"type\": \"module\",\n  \"name\": \"prettier-plugin-tailwindcss\",\n  \"version\": \"0.7.2\",\n  \"description\": \"A Prettier plugin for sorting Tailwind CSS classes.\",\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.mjs\",\n  \"module\": \"dist/index.mjs\",\n  \"types\": \"dist/index.d.mts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.mts\",\n      \"import\": \"./dist/index.mjs\",\n      \"default\": \"./dist/index.mjs\"\n    },\n    \"./sorter\": {\n      \"types\": \"./dist/sorter.d.mts\",\n      \"import\": \"./dist/sorter.mjs\",\n      \"default\": \"./dist/sorter.mjs\"\n    }\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/tailwindlabs/prettier-plugin-tailwindcss\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues\"\n  },\n  \"scripts\": {\n    \"build\": \"tsdown\",\n    \"dev\": \"tsdown --watch\",\n    \"pretest\": \"node scripts/install-fixture-deps.js\",\n    \"test\": \"vitest\",\n    \"prepublishOnly\": \"npm run build && node scripts/copy-licenses.js\",\n    \"format\": \"prettier \\\"src/**/*.ts\\\" \\\"scripts/**/*.js\\\" \\\"tests/*.ts\\\" --write --print-width 100 --single-quote --no-semi\",\n    \"lint\": \"knip && oxlint --type-aware src tests/*.ts\",\n    \"release-channel\": \"node ./scripts/release-channel.js\",\n    \"release-notes\": \"node ./scripts/release-notes.js\"\n  },\n  \"devDependencies\": {\n    \"@babel/types\": \"^7.28.2\",\n    \"@ianvs/prettier-plugin-sort-imports\": \"^4.7.0\",\n    \"@marko/compiler\": \"^5.39.49\",\n    \"@prettier/plugin-hermes\": \"^0.1.2\",\n    \"@prettier/plugin-oxc\": \"^0.1.2\",\n    \"@prettier/plugin-pug\": \"^3.4.2\",\n    \"@shopify/prettier-plugin-liquid\": \"^1.10.0\",\n    \"@trivago/prettier-plugin-sort-imports\": \"^6.0.0\",\n    \"@types/node\": \"^24.3.0\",\n    \"@zackad/prettier-plugin-twig\": \"^0.16.0\",\n    \"clear-module\": \"^4.1.2\",\n    \"dedent\": \"^1.6.0\",\n    \"enhanced-resolve\": \"^5.18.3\",\n    \"escalade\": \"^3.2.0\",\n    \"fast-glob\": \"^3.3.3\",\n    \"import-sort-style-module\": \"^6.0.0\",\n    \"jiti\": \"^2.6.0\",\n    \"knip\": \"^5.83.0\",\n    \"license-checker\": \"^25.0.1\",\n    \"line-column\": \"^1.0.2\",\n    \"marko\": \"^5.37.46\",\n    \"oxlint\": \"^1.43.0\",\n    \"oxlint-tsgolint\": \"^0.11.4\",\n    \"postcss\": \"^8.5.6\",\n    \"postcss-import\": \"^16.1.1\",\n    \"prettier\": \"^3.7.3\",\n    \"prettier-plugin-astro\": \"^0.14.1\",\n    \"prettier-plugin-css-order\": \"^2.1.2\",\n    \"prettier-plugin-jsdoc\": \"^1.3.3\",\n    \"prettier-plugin-marko\": \"^3.3.2\",\n    \"prettier-plugin-multiline-arrays\": \"^4.0.3\",\n    \"prettier-plugin-organize-attributes\": \"^1.0.0\",\n    \"prettier-plugin-organize-imports\": \"^4.2.0\",\n    \"prettier-plugin-sort-imports\": \"^1.8.8\",\n    \"prettier-plugin-svelte\": \"^3.4.0\",\n    \"pug-lexer\": \"^5.0.1\",\n    \"svelte\": \"^5.38.2\",\n    \"tailwindcss-v3\": \"npm:tailwindcss@^3.4.18\",\n    \"tailwindcss-v4\": \"npm:tailwindcss@^4.1.14\",\n    \"tsdown\": \"^0.20.1\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"peerDependencies\": {\n    \"@ianvs/prettier-plugin-sort-imports\": \"*\",\n    \"@prettier/plugin-hermes\": \"*\",\n    \"@prettier/plugin-oxc\": \"*\",\n    \"@prettier/plugin-pug\": \"*\",\n    \"@shopify/prettier-plugin-liquid\": \"*\",\n    \"@trivago/prettier-plugin-sort-imports\": \"*\",\n    \"@zackad/prettier-plugin-twig\": \"*\",\n    \"prettier\": \"^3.0\",\n    \"prettier-plugin-astro\": \"*\",\n    \"prettier-plugin-css-order\": \"*\",\n    \"prettier-plugin-jsdoc\": \"*\",\n    \"prettier-plugin-marko\": \"*\",\n    \"prettier-plugin-multiline-arrays\": \"*\",\n    \"prettier-plugin-organize-attributes\": \"*\",\n    \"prettier-plugin-organize-imports\": \"*\",\n    \"prettier-plugin-sort-imports\": \"*\",\n    \"prettier-plugin-svelte\": \"*\"\n  },\n  \"peerDependenciesMeta\": {\n    \"@ianvs/prettier-plugin-sort-imports\": {\n      \"optional\": true\n    },\n    \"@prettier/plugin-hermes\": {\n      \"optional\": true\n    },\n    \"@prettier/plugin-oxc\": {\n      \"optional\": true\n    },\n    \"@prettier/plugin-pug\": {\n      \"optional\": true\n    },\n    \"@shopify/prettier-plugin-liquid\": {\n      \"optional\": true\n    },\n    \"@trivago/prettier-plugin-sort-imports\": {\n      \"optional\": true\n    },\n    \"@zackad/prettier-plugin-twig\": {\n      \"optional\": true\n    },\n    \"prettier-plugin-astro\": {\n      \"optional\": true\n    },\n    \"prettier-plugin-css-order\": {\n      \"optional\": true\n    },\n    \"prettier-plugin-jsdoc\": {\n      \"optional\": true\n    },\n    \"prettier-plugin-marko\": {\n      \"optional\": true\n    },\n    \"prettier-plugin-multiline-arrays\": {\n      \"optional\": true\n    },\n    \"prettier-plugin-organize-attributes\": {\n      \"optional\": true\n    },\n    \"prettier-plugin-organize-imports\": {\n      \"optional\": true\n    },\n    \"prettier-plugin-sort-imports\": {\n      \"optional\": true\n    },\n    \"prettier-plugin-svelte\": {\n      \"optional\": true\n    }\n  },\n  \"engines\": {\n    \"node\": \">=20.19\"\n  },\n  \"importSort\": {\n    \".js, .jsx, .ts, .tsx\": {\n      \"style\": \"module\"\n    }\n  },\n  \"jest\": {\n    \"testTimeout\": 15000\n  },\n  \"overrides\": {\n    \"@ianvs/prettier-plugin-sort-imports\": {\n      \"@prettier/plugin-oxc\": \"^0.1.2\"\n    }\n  },\n  \"packageManager\": \"pnpm@10.28.2\"\n}\n"
  },
  {
    "path": "prettier.config.js",
    "content": "/** @type {import('prettier').Config} */\nexport default {\n  semi: false,\n  singleQuote: true,\n  trailingComma: 'all',\n  plugins: ['@ianvs/prettier-plugin-sort-imports'],\n  importOrder: ['^@', '^[a-zA-Z0-9-]+', '^[./]'],\n  printWidth: 120,\n}\n"
  },
  {
    "path": "scripts/copy-licenses.js",
    "content": "import * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport checker from 'license-checker'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\n\nconst pkg = JSON.parse(await fs.readFile(path.resolve(__dirname, '../package.json'), 'utf8'))\n\nlet exclude = ['vitest', 'license-checker', 'prettier', 'svelte', 'knip']\n\n/** @type {checker.ModuleInfo} */\nlet packages = await new Promise((resolve, reject) => {\n  checker.init({ start: path.resolve(__dirname, '..') }, (_err, packages) => {\n    if (_err) {\n      reject(_err)\n    } else {\n      resolve(packages)\n    }\n  })\n})\n\nfor (let key in packages) {\n  let dep = packages[key]\n  let name = key.split(/(?<=.)@/)[0]\n\n  if (exclude.includes(name)) continue\n  if (!dep.licenseFile) continue\n  if (!(name in pkg.devDependencies)) continue\n\n  let dir = path.resolve(__dirname, '../dist/licenses', name)\n  await fs.mkdir(dir, { recursive: true })\n  await fs.copyFile(dep.licenseFile, path.resolve(dir, path.basename(dep.licenseFile)))\n}\n"
  },
  {
    "path": "scripts/install-fixture-deps.js",
    "content": "import { exec } from 'node:child_process'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { promisify } from 'node:util'\nimport glob from 'fast-glob'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\n\nconst fixtures = glob.sync(\n  [\n    'tests/fixtures/*/package.json',\n    'tests/fixtures/v4/*/package.json',\n    'tests/fixtures/monorepo/*/package.json',\n  ],\n  {\n    cwd: path.resolve(__dirname, '..'),\n  },\n)\n\nconst execAsync = promisify(exec)\n\nawait Promise.all(\n  fixtures.map(async (fixture) => {\n    console.log(`Installing dependencies for ${fixture}`)\n\n    await execAsync('npm install', { cwd: path.dirname(fixture) })\n  }),\n)\n"
  },
  {
    "path": "scripts/release-channel.js",
    "content": "// Given a version, figure out what the release channel is so that we can publish to the correct\n// channel on npm.\n//\n// E.g.:\n//\n//   1.2.3                  -> latest (default)\n//   0.0.0-insiders.ffaa88  -> insiders\n//   4.1.0-alpha.4          -> alpha\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\n\nconst pkg = JSON.parse(await fs.readFile(path.resolve(__dirname, '../package.json'), 'utf8'))\n\nlet version = process.argv[2] || process.env.npm_package_version || pkg.version\n\nlet match = /\\d+\\.\\d+\\.\\d+-(.*)\\.\\d+/g.exec(version)\nif (match) {\n  console.log(match[1])\n} else {\n  console.log('latest')\n}\n"
  },
  {
    "path": "scripts/release-notes.js",
    "content": "// 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 GitHub release for the current version.\n\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url))\n\nconst pkg = JSON.parse(await fs.readFile(path.resolve(__dirname, '../package.json'), 'utf8'))\n\nlet version = process.argv[2] || process.env.npm_package_version || pkg.version\n\nlet changelog = await fs.readFile(path.resolve(__dirname, '..', 'CHANGELOG.md'), 'utf8')\nlet match = new RegExp(\n  `## \\\\[${version}\\\\] - (.*)\\\\n\\\\n([\\\\s\\\\S]*?)\\\\n(?:(?:##\\\\s)|(?:\\\\[))`,\n  'g',\n).exec(changelog)\n\nif (match) {\n  let [, , notes] = match\n  console.log(notes.trim())\n} else {\n  console.log(`Placeholder release notes for version: v${version}`)\n}\n"
  },
  {
    "path": "src/config.ts",
    "content": "// @ts-check\nimport * as path from 'node:path'\nimport prettier from 'prettier'\nimport type { ParserOptions } from 'prettier'\nimport * as console from './console'\nimport { expiringMap } from './expiring-map.js'\nimport { getTailwindConfig as getTailwindConfigFromLib } from './sorter.js'\nimport type { UnifiedApi } from './types'\nimport { cacheForDirs } from './utils.js'\n\nlet prettierConfigCache = expiringMap<string, string | null>(10_000)\n\nasync function resolvePrettierConfigDir(\n  filePath: string,\n  inputDir: string,\n): Promise<string> {\n  // Check cache for this directory\n  let cached = prettierConfigCache.get(inputDir)\n  if (cached !== undefined) {\n    return cached ?? process.cwd()\n  }\n\n  const resolve = async () => {\n    try {\n      return await prettier.resolveConfigFile(filePath)\n    } catch (err) {\n      console.error('prettier-config-not-found', 'Failed to resolve Prettier Config')\n      console.error('prettier-config-not-found-err', err)\n      return null\n    }\n  }\n\n  let prettierConfig = await resolve()\n\n  // Cache all directories from inputDir up to config location\n  if (prettierConfig) {\n    let configDir = path.dirname(prettierConfig)\n    cacheForDirs(prettierConfigCache, inputDir, configDir, configDir)\n    return configDir\n  } else {\n    prettierConfigCache.set(inputDir, null)\n    return process.cwd()\n  }\n}\n\nexport async function getTailwindConfig(options: ParserOptions): Promise<UnifiedApi> {\n  let cwd = process.cwd()\n  let inputDir = options.filepath ? path.dirname(options.filepath) : cwd\n\n  // Only resolve prettier config dir if we need it for relative path resolution\n  let needsPrettierConfig =\n    (options.tailwindConfig && !path.isAbsolute(options.tailwindConfig)) ||\n    (options.tailwindStylesheet && !path.isAbsolute(options.tailwindStylesheet)) ||\n    (options.tailwindEntryPoint && !path.isAbsolute(options.tailwindEntryPoint))\n\n  let configDir: string\n  if (needsPrettierConfig) {\n    configDir = await resolvePrettierConfigDir(options.filepath, inputDir)\n  } else {\n    configDir = cwd\n  }\n\n  let configPath =\n    options.tailwindConfig && !options.tailwindConfig.endsWith('.css')\n      ? options.tailwindConfig\n      : undefined\n\n  let stylesheetPath = options.tailwindStylesheet\n  if (!stylesheetPath && options.tailwindEntryPoint) {\n    console.warn(\n      'entrypoint-is-deprecated',\n      configDir,\n      'Deprecated: Use the `tailwindStylesheet` option for v4 projects instead of `tailwindEntryPoint`.',\n    )\n    stylesheetPath = options.tailwindEntryPoint\n  }\n\n  if (!stylesheetPath && options.tailwindConfig && options.tailwindConfig.endsWith('.css')) {\n    console.warn(\n      'config-as-css-is-deprecated',\n      configDir,\n      'Deprecated: Use the `tailwindStylesheet` option for v4 projects instead of `tailwindConfig`.',\n    )\n    stylesheetPath = options.tailwindConfig\n  }\n\n  return getTailwindConfigFromLib({\n    base: configDir,\n    filepath: options.filepath,\n    configPath,\n    stylesheetPath,\n    packageName: options.tailwindPackageName,\n  })\n}\n"
  },
  {
    "path": "src/console.ts",
    "content": "let seen = new Set<string>()\n\nexport function log(key: string, arg: unknown, ...args: unknown[]) {\n  if (seen.has(key)) return\n  seen.add(key)\n  console.log(arg, ...args)\n}\n\nexport function warn(key: string, arg: unknown, ...args: unknown[]) {\n  if (seen.has(key)) return\n  seen.add(key)\n  console.warn(arg, ...args)\n}\n\nexport function error(key: string, arg: unknown, ...args: unknown[]) {\n  if (seen.has(key)) return\n  seen.add(key)\n  console.error(arg, ...args)\n}\n"
  },
  {
    "path": "src/create-plugin.ts",
    "content": "import { isAbsolute } from 'path'\nimport type { Parser, ParserOptions, Plugin, Printer } from 'prettier'\nimport { getTailwindConfig } from './config'\nimport { createMatcher } from './options'\nimport { loadIfExists, maybeResolve } from './resolve'\nimport type { PluginLoad, TransformOptions } from './transform'\nimport type { TransformerEnv } from './types'\n\nexport function createPlugin(transforms: TransformOptions<any>[]) {\n  // Prettier parsers and printers may be async functions at definition time.\n  // They'll be awaited when the plugin is loaded but must also be swapped out\n  // with the resolved value before returning as later Prettier internals\n  // assume that parsers and printers are objects and not functions.\n  type Init<T> = (() => Promise<T | undefined>) | T | undefined\n\n  let parsers: Record<string, Init<Parser<any>>> = Object.create(null)\n  let printers: Record<string, Init<Printer<any>>> = Object.create(null)\n\n  for (let opts of transforms) {\n    for (let [name, meta] of Object.entries(opts.parsers)) {\n      parsers[name] = async () => {\n        let plugin = await loadPlugins(meta.load ?? opts.load ?? [])\n        let original = plugin.parsers?.[name]\n        if (!original) return\n\n        parsers[name] = await createParser({\n          name,\n          original,\n          opts,\n        })\n\n        return parsers[name]\n      }\n    }\n\n    for (let [name, _meta] of Object.entries(opts.printers ?? {})) {\n      printers[name] = async () => {\n        let plugin = await loadPlugins(opts.load ?? [])\n        let original = plugin.printers?.[name]\n        if (!original) return\n\n        printers[name] = createPrinter({\n          original,\n          opts,\n        })\n\n        return printers[name]\n      }\n    }\n  }\n\n  return { parsers, printers }\n}\n\nasync function createParser({\n  name,\n  original,\n  opts,\n}: {\n  name: string\n  original: Parser<any>\n  opts: TransformOptions<any>\n}) {\n  let parser: Parser<any> = { ...original }\n\n  async function load(options: ParserOptions<any>) {\n    let parser: Parser<any> = { ...original }\n\n    for (const pluginName of opts.compatible || []) {\n      let plugin = await findEnabledPlugin(options, pluginName)\n      if (plugin?.parsers?.[name]) Object.assign(parser, plugin.parsers[name])\n    }\n\n    return parser\n  }\n\n  parser.preprocess = async (code: string, options: ParserOptions) => {\n    let parser = await load(options)\n    return parser.preprocess ? parser.preprocess(code, options) : code\n  }\n\n  parser.parse = async (code, options) => {\n    let original = await load(options)\n\n    // @ts-expect-error: `options` is passed twice for compat with older plugins that were written\n    // for Prettier v2 but still work with v3.\n    //\n    // Currently only the Twig plugin requires this.\n    let ast = await original.parse(code, options, options)\n\n    let env = await loadTailwindCSS({ opts, options })\n\n    transformAst({\n      ast,\n      env,\n      opts,\n      options,\n    })\n\n    options.__tailwindcss__ = env\n\n    return ast\n  }\n\n  return parser\n}\n\nfunction createPrinter({\n  original,\n  opts,\n}: {\n  original: Printer<any>\n  opts: TransformOptions<any>\n}) {\n  let printer: Printer<any> = { ...original }\n\n  let reprint = opts.reprint\n\n  // Hook into the preprocessing phase to load the config\n  if (reprint) {\n    printer.print = new Proxy(original.print, {\n      apply(target, thisArg, args) {\n        let [path, options] = args as Parameters<typeof original.print>\n        let env = options.__tailwindcss__ as TransformerEnv\n        reprint(path, { ...env, options: options })\n        return Reflect.apply(target, thisArg, args)\n      },\n    })\n\n    if (original.embed) {\n      printer.embed = new Proxy(original.embed, {\n        apply(target, thisArg, args) {\n          let [path, options] = args as Parameters<typeof original.embed>\n          let env = options.__tailwindcss__ as TransformerEnv\n          reprint(path, { ...env, options: options as any })\n          return Reflect.apply(target, thisArg, args)\n        },\n      })\n    }\n  }\n\n  return printer\n}\n\nasync function loadPlugins<T>(fns: PluginLoad[]) {\n  let plugin: Plugin<T> = {\n    parsers: Object.create(null),\n    printers: Object.create(null),\n    options: Object.create(null),\n    defaultOptions: Object.create(null),\n    languages: [],\n  }\n\n  for (let source of fns) {\n    let loaded = await loadPlugin(source)\n    Object.assign(plugin.parsers!, loaded.parsers ?? {})\n    Object.assign(plugin.printers!, loaded.printers ?? {})\n    Object.assign(plugin.options!, loaded.options ?? {})\n    Object.assign(plugin.defaultOptions!, loaded.defaultOptions ?? {})\n\n    plugin.languages = [...(plugin.languages ?? []), ...(loaded.languages ?? [])]\n  }\n\n  return plugin\n}\n\nconst EMPTY_PLUGIN: Plugin<any> = {\n  parsers: {},\n  printers: {},\n  languages: [],\n  options: {},\n  defaultOptions: {},\n}\n\nasync function loadPlugin(source: PluginLoad): Promise<Plugin<any>> {\n  if ('importer' in source && typeof source.importer === 'function') {\n    return normalizePlugin(await source.importer())\n  }\n\n  return source\n}\n\nfunction normalizePlugin(source: unknown): Plugin<any> {\n  if (source === null || typeof source !== 'object') return EMPTY_PLUGIN\n  let maybeModule = source as { default?: unknown }\n  let plugin = maybeModule.default\n  return (plugin && typeof plugin === 'object' ? plugin : source) as Plugin<any>\n}\n\nfunction findEnabledPlugin(options: ParserOptions<any>, name: string) {\n  for (let plugin of options.plugins) {\n    if (plugin instanceof URL) {\n      if (plugin.protocol !== 'file:') continue\n      if (plugin.hostname !== '') continue\n\n      plugin = plugin.pathname\n    }\n\n    if (typeof plugin !== 'string') {\n      if (!plugin.name) {\n        continue\n      }\n      plugin = plugin.name\n    }\n\n    if (\n      plugin === name ||\n      (isAbsolute(plugin) && plugin.includes(name) && maybeResolve(name) === plugin)\n    ) {\n      return loadIfExists<Plugin<any>>(name)\n    }\n  }\n}\n\nasync function loadTailwindCSS<T = any>({\n  options,\n  opts,\n}: {\n  options: ParserOptions<T>\n  opts: TransformOptions<T>\n}): Promise<TransformerEnv> {\n  let parsers = opts.parsers\n  let parser = options.parser as string\n\n  let context = await getTailwindConfig(options)\n\n  let matcher = createMatcher(options, parser, {\n    staticAttrs: new Set(parsers[parser]?.staticAttrs ?? opts.staticAttrs ?? []),\n    dynamicAttrs: new Set(parsers[parser]?.dynamicAttrs ?? opts.dynamicAttrs ?? []),\n    functions: new Set(),\n    staticAttrsRegex: [],\n    dynamicAttrsRegex: [],\n    functionsRegex: [],\n  })\n\n  return {\n    context,\n    matcher,\n    options,\n    changes: [],\n  }\n}\n\nfunction transformAst<T = any>({\n  ast,\n  env,\n  opts,\n}: {\n  ast: T\n  env: TransformerEnv\n  options: ParserOptions<T>\n  opts: TransformOptions<T>\n}) {\n  let transform = opts.transform\n  if (transform) transform(ast, env)\n}\n"
  },
  {
    "path": "src/expiring-map.ts",
    "content": "interface ExpiringMap<K, V> {\n  get(key: K): V | undefined\n  remember(key: K, factory: () => V): V\n  set(key: K, value: V): void\n}\n\nexport function expiringMap<K, V>(duration: number): ExpiringMap<K, V> {\n  let map = new Map<K, { value: V; expiration: Date }>()\n\n  return {\n    get(key: K) {\n      let result = map.get(key)\n\n      if (result && result.expiration > new Date()) {\n        return result.value\n      }\n\n      map.delete(key)\n\n      return undefined\n    },\n\n    remember(key: K, factory: () => V) {\n      let result = map.get(key)\n\n      if (result && result.expiration > new Date()) {\n        return result.value\n      }\n\n      map.delete(key)\n\n      let value = factory()\n      this.set(key, value)\n      return value\n    },\n\n    set(key: K, value: V) {\n      let expiration = new Date()\n      expiration.setMilliseconds(expiration.getMilliseconds() + duration)\n\n      map.set(key, {\n        value,\n        expiration,\n      })\n    },\n  }\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "// @ts-ignore\nimport type * as Liquid from '@shopify/prettier-plugin-liquid/dist/types.js'\n// @ts-ignore\nimport lineColumn from 'line-column'\nimport * as prettierParserAngular from 'prettier/plugins/angular'\nimport * as prettierParserBabel from 'prettier/plugins/babel'\nimport * as prettierParserCss from 'prettier/plugins/postcss'\n// @ts-ignore\nimport { createPlugin } from './create-plugin.js'\nimport type { Matcher } from './options.js'\nimport { sortClasses, sortClassList } from './sorting.js'\nimport { defineTransform } from './transform.js'\nimport type { StringChange, TransformerEnv } from './types'\nimport { spliceChangesIntoString, visit, type Path } from './utils.js'\n\nconst ESCAPE_SEQUENCE_PATTERN = /\\\\(['\"\\\\nrtbfv0-7xuU])/g\nfunction tryParseAngularAttribute(value: string, env: TransformerEnv) {\n  try {\n    return prettierParserAngular.parsers.__ng_directive.parse(value, env.options)\n  } catch (err) {\n    console.warn('prettier-plugin-tailwindcss: Unable to parse angular directive')\n    console.warn(err)\n    return null\n  }\n}\n\nfunction transformDynamicAngularAttribute(attr: any, env: TransformerEnv) {\n  let directiveAst = tryParseAngularAttribute(attr.value, env)\n\n  // If we've reached this point we couldn't parse the expression we we should bail\n  // `tryParseAngularAttribute` will display some warnings/errors\n  // But we shouldn't fail outright — just miss parsing some attributes\n  if (!directiveAst) {\n    return\n  }\n\n  let changes: StringChange[] = []\n\n  visit(directiveAst, {\n    StringLiteral(node, path) {\n      if (!node.value) return\n\n      let collapseWhitespace = canCollapseWhitespaceIn(path, env)\n\n      changes.push({\n        start: node.start + 1,\n        end: node.end - 1,\n        before: node.value,\n        after: sortClasses(node.value, {\n          env,\n          collapseWhitespace,\n        }),\n      })\n    },\n\n    TemplateLiteral(node, path) {\n      if (!node.quasis.length) return\n\n      let collapseWhitespace = canCollapseWhitespaceIn(path, env)\n\n      for (let i = 0; i < node.quasis.length; i++) {\n        let quasi = node.quasis[i]\n\n        changes.push({\n          start: quasi.start,\n          end: quasi.end,\n          before: quasi.value.raw,\n          after: sortClasses(quasi.value.raw, {\n            env,\n\n            // Is not the first \"item\" and does not start with a space\n            ignoreFirst: i > 0 && !/^\\s/.test(quasi.value.raw),\n\n            // Is between two expressions\n            // And does not end with a space\n            ignoreLast: i < node.expressions.length && !/\\s$/.test(quasi.value.raw),\n\n            collapseWhitespace: collapseWhitespace\n              ? {\n                  start: collapseWhitespace.start && i === 0,\n                  end: collapseWhitespace.end && i >= node.expressions.length,\n                }\n              : false,\n          }),\n        })\n      }\n    },\n  })\n\n  attr.value = spliceChangesIntoString(attr.value, changes)\n}\n\nfunction transformDynamicJsAttribute(attr: any, env: TransformerEnv) {\n  let { matcher } = env\n\n  let expressionPrefix = 'let __prettier_temp__ = '\n  let source = `${expressionPrefix}${attr.value}`\n  let ast = prettierParserBabel.parsers['babel-ts'].parse(source, env.options)\n\n  let didChange = false\n  let changes: StringChange[] = []\n\n  function findConcatEntry(path: Path<any, any>) {\n    return path.find(\n      (entry) => entry.parent?.type === 'BinaryExpression' && entry.parent.operator === '+',\n    )\n  }\n\n  function addChange(\n    start: number | null | undefined,\n    end: number | null | undefined,\n    after: string,\n  ) {\n    if (start == null || end == null) return\n\n    let offsetStart = start - expressionPrefix.length\n    let offsetEnd = end - expressionPrefix.length\n\n    if (offsetStart < 0 || offsetEnd < 0) return\n\n    didChange = true\n    changes.push({\n      start: offsetStart,\n      end: offsetEnd,\n      before: attr.value.slice(offsetStart, offsetEnd),\n      after,\n    })\n  }\n\n  visit(ast, {\n    StringLiteral(node, path) {\n      let concat = findConcatEntry(path)\n      let sorted = sortStringLiteral(node, {\n        env,\n        collapseWhitespace: {\n          start: concat?.key !== 'right',\n          end: concat?.key !== 'left',\n        },\n      })\n\n      if (sorted) {\n        // @ts-ignore\n        let raw = node.extra?.raw ?? node.raw\n        if (typeof raw === 'string') {\n          addChange(node.start, node.end, raw)\n        }\n      }\n    },\n\n    Literal(node: any, path) {\n      if (!isStringLiteral(node)) return\n\n      let concat = findConcatEntry(path)\n      let sorted = sortStringLiteral(node, {\n        env,\n        collapseWhitespace: {\n          start: concat?.key !== 'right',\n          end: concat?.key !== 'left',\n        },\n      })\n\n      if (sorted) {\n        // @ts-ignore\n        let raw = node.extra?.raw ?? node.raw\n        if (typeof raw === 'string') {\n          addChange(node.start, node.end, raw)\n        }\n      }\n    },\n\n    TemplateLiteral(node, path) {\n      let concat = findConcatEntry(path)\n      let originalQuasis = node.quasis.map((quasi) => quasi.value.raw)\n      let sorted = sortTemplateLiteral(node, {\n        env,\n        collapseWhitespace: {\n          start: concat?.key !== 'right',\n          end: concat?.key !== 'left',\n        },\n      })\n\n      if (sorted) {\n        for (let i = 0; i < node.quasis.length; i++) {\n          let quasi = node.quasis[i]\n          if (quasi.value.raw !== originalQuasis[i]) {\n            addChange(quasi.start, quasi.end, quasi.value.raw)\n          }\n        }\n      }\n    },\n\n    TaggedTemplateExpression(node, path) {\n      if (!isSortableTemplateExpression(node, matcher)) {\n        return\n      }\n\n      let concat = findConcatEntry(path)\n      let originalQuasis = node.quasi.quasis.map((quasi) => quasi.value.raw)\n      let sorted = sortTemplateLiteral(node.quasi, {\n        env,\n        collapseWhitespace: {\n          start: concat?.key !== 'right',\n          end: concat?.key !== 'left',\n        },\n      })\n\n      if (sorted) {\n        for (let i = 0; i < node.quasi.quasis.length; i++) {\n          let quasi = node.quasi.quasis[i]\n          if (quasi.value.raw !== originalQuasis[i]) {\n            addChange(quasi.start, quasi.end, quasi.value.raw)\n          }\n        }\n      }\n    },\n  })\n\n  if (didChange) {\n    attr.value = spliceChangesIntoString(attr.value, changes)\n  }\n}\n\nfunction transformHtml(ast: any, env: TransformerEnv) {\n  let { matcher } = env\n  let { parser } = env.options\n\n  for (let attr of ast.attrs ?? []) {\n    if (matcher.hasStaticAttr(attr.name)) {\n      attr.value = sortClasses(attr.value, { env })\n    } else if (matcher.hasDynamicAttr(attr.name)) {\n      if (!/[`'\"]/.test(attr.value)) {\n        continue\n      }\n\n      if (parser === 'angular') {\n        transformDynamicAngularAttribute(attr, env)\n      } else {\n        transformDynamicJsAttribute(attr, env)\n      }\n    }\n  }\n\n  for (let child of ast.children ?? []) {\n    transformHtml(child, env)\n  }\n}\n\nfunction transformGlimmer(ast: any, env: TransformerEnv) {\n  let { matcher } = env\n\n  visit(ast, {\n    AttrNode(attr, _path, meta) {\n      if (matcher.hasStaticAttr(attr.name) && attr.value) {\n        meta.sortTextNodes = true\n      }\n    },\n\n    TextNode(node, path, meta) {\n      if (!meta.sortTextNodes) {\n        return\n      }\n\n      let concat = path.find((entry) => {\n        return entry.parent && entry.parent.type === 'ConcatStatement'\n      })\n\n      let siblings = {\n        prev: concat?.parent.parts[concat.index! - 1],\n        next: concat?.parent.parts[concat.index! + 1],\n      }\n\n      node.chars = sortClasses(node.chars, {\n        env,\n        ignoreFirst: siblings.prev && !/^\\s/.test(node.chars),\n        ignoreLast: siblings.next && !/\\s$/.test(node.chars),\n        collapseWhitespace: {\n          start: !siblings.prev,\n          end: !siblings.next,\n        },\n      })\n    },\n\n    StringLiteral(node, path, meta) {\n      if (!meta.sortTextNodes) {\n        return\n      }\n\n      let concat = path.find((entry) => {\n        return (\n          entry.parent &&\n          entry.parent.type === 'SubExpression' &&\n          entry.parent.path.original === 'concat'\n        )\n      })\n\n      node.value = sortClasses(node.value, {\n        env,\n        ignoreLast: Boolean(concat) && !/[^\\S\\r\\n]$/.test(node.value),\n        collapseWhitespace: {\n          start: false,\n          end: !concat,\n        },\n      })\n    },\n  })\n}\n\nfunction transformLiquid(ast: any, env: TransformerEnv) {\n  let { matcher } = env\n\n  function isClassAttr(node: { name: string | { type: string; value: string }[] }) {\n    return Array.isArray(node.name)\n      ? node.name.every((n) => n.type === 'TextNode' && matcher.hasStaticAttr(n.value))\n      : matcher.hasStaticAttr(node.name)\n  }\n\n  function hasSurroundingQuotes(str: string) {\n    let start = str[0]\n    let end = str[str.length - 1]\n\n    return start === end && (start === '\"' || start === \"'\" || start === '`')\n  }\n\n  let sources: { type: string; source: string }[] = []\n\n  let changes: StringChange[] = []\n\n  function sortAttribute(attr: Liquid.AttrSingleQuoted | Liquid.AttrDoubleQuoted) {\n    for (let i = 0; i < attr.value.length; i++) {\n      let node = attr.value[i]\n      if (node.type === 'TextNode') {\n        let after = sortClasses(node.value, {\n          env,\n          ignoreFirst: i > 0 && !/^\\s/.test(node.value),\n          ignoreLast: i < attr.value.length - 1 && !/\\s$/.test(node.value),\n          removeDuplicates: false,\n          collapseWhitespace: false,\n        })\n\n        changes.push({\n          start: node.position.start,\n          end: node.position.end,\n          before: node.value,\n          after,\n        })\n      } else if (\n        // @ts-ignore: `LiquidDrop` is for older versions of the liquid plugin (1.2.x)\n        (node.type === 'LiquidDrop' || node.type === 'LiquidVariableOutput') &&\n        typeof node.markup === 'object' &&\n        node.markup.type === 'LiquidVariable'\n      ) {\n        visit(node.markup.expression, {\n          String(node: any) {\n            let pos = { ...node.position }\n\n            // We have to offset the position ONLY when quotes are part of the String node\n            // This is because `value` does NOT include quotes\n            if (hasSurroundingQuotes(node.source.slice(pos.start, pos.end))) {\n              pos.start += 1\n              pos.end -= 1\n            }\n\n            let after = sortClasses(node.value, { env })\n\n            changes.push({\n              start: pos.start,\n              end: pos.end,\n              before: node.value,\n              after,\n            })\n          },\n        })\n      }\n    }\n  }\n\n  visit(ast, {\n    LiquidTag(node: any) {\n      sources.push(node)\n    },\n\n    HtmlElement(node: any) {\n      sources.push(node)\n    },\n\n    AttrSingleQuoted(node: any) {\n      if (isClassAttr(node)) {\n        sources.push(node)\n        sortAttribute(node)\n      }\n    },\n\n    AttrDoubleQuoted(node: any) {\n      if (isClassAttr(node)) {\n        sources.push(node)\n        sortAttribute(node)\n      }\n    },\n  })\n\n  for (let node of sources) {\n    node.source = spliceChangesIntoString(node.source, changes)\n  }\n}\n\nfunction sortStringLiteral(\n  node: any,\n  {\n    env,\n    removeDuplicates,\n    collapseWhitespace = { start: true, end: true },\n  }: {\n    env: TransformerEnv\n    removeDuplicates?: false\n    collapseWhitespace?: false | { start: boolean; end: boolean }\n  },\n) {\n  let result = sortClasses(node.value, {\n    env,\n    removeDuplicates,\n    collapseWhitespace,\n  })\n\n  let didChange = result !== node.value\n\n  if (!didChange) return false\n\n  node.value = result\n\n  // Preserve the original escaping level for the new content\n  let raw = node.extra?.raw ?? node.raw\n  let quote = raw[0]\n  let originalRawContent = raw.slice(1, -1)\n  let originalValue = node.extra?.rawValue ?? node.value\n\n  if (node.extra) {\n    // The original list has ecapes so we ensure that the sorted list also\n    // maintains those by replacing backslashes from escape sequences.\n    //\n    // It seems that TypeScript-based ASTs don't need this special handling\n    // which is why this is guarded inside the `node.extra` check\n    if (originalRawContent !== originalValue && originalValue.includes('\\\\')) {\n      result = result.replace(ESCAPE_SEQUENCE_PATTERN, '\\\\\\\\$1')\n    }\n\n    // JavaScript (StringLiteral)\n    node.extra = {\n      ...node.extra,\n      rawValue: result,\n      raw: quote + result + quote,\n    }\n  } else {\n    // TypeScript (Literal)\n    node.raw = quote + result + quote\n  }\n\n  return true\n}\n\nfunction isStringLiteral(node: any) {\n  return (\n    node.type === 'StringLiteral' || (node.type === 'Literal' && typeof node.value === 'string')\n  )\n}\n\nfunction sortTemplateLiteral(\n  node: any,\n  {\n    env,\n    removeDuplicates,\n    collapseWhitespace = { start: true, end: true },\n  }: {\n    env: TransformerEnv\n    removeDuplicates?: false\n    collapseWhitespace?: false | { start: boolean; end: boolean }\n  },\n) {\n  let didChange = false\n\n  for (let i = 0; i < node.quasis.length; i++) {\n    let quasi = node.quasis[i]\n    let same = quasi.value.raw === quasi.value.cooked\n    let originalRaw = quasi.value.raw\n    let originalCooked = quasi.value.cooked\n\n    quasi.value.raw = sortClasses(quasi.value.raw, {\n      env,\n      removeDuplicates,\n      // Is not the first \"item\" and does not start with a space\n      ignoreFirst: i > 0 && !/^\\s/.test(quasi.value.raw),\n\n      // Is between two expressions\n      // And does not end with a space\n      ignoreLast: i < node.expressions.length && !/\\s$/.test(quasi.value.raw),\n\n      collapseWhitespace: collapseWhitespace && {\n        start: collapseWhitespace && collapseWhitespace.start && i === 0,\n        end: collapseWhitespace && collapseWhitespace.end && i >= node.expressions.length,\n      },\n    })\n\n    quasi.value.cooked = same\n      ? quasi.value.raw\n      : sortClasses(quasi.value.cooked, {\n          env,\n          ignoreFirst: i > 0 && !/^\\s/.test(quasi.value.cooked),\n          ignoreLast: i < node.expressions.length && !/\\s$/.test(quasi.value.cooked),\n          removeDuplicates,\n          collapseWhitespace: collapseWhitespace && {\n            start: collapseWhitespace && collapseWhitespace.start && i === 0,\n            end: collapseWhitespace && collapseWhitespace.end && i >= node.expressions.length,\n          },\n        })\n\n    if (quasi.value.raw !== originalRaw || quasi.value.cooked !== originalCooked) {\n      didChange = true\n    }\n  }\n\n  return didChange\n}\n\nfunction isSortableTemplateExpression(\n  node: import('@babel/types').TaggedTemplateExpression,\n  matcher: Matcher,\n): boolean {\n  return isSortableExpression(node.tag, matcher)\n}\n\nfunction isSortableCallExpression(\n  node: import('@babel/types').CallExpression,\n  matcher: Matcher,\n): boolean {\n  if (!node.arguments?.length) return false\n\n  return isSortableExpression(node.callee, matcher)\n}\n\nfunction isSortableExpression(\n  node: import('@babel/types').Expression | import('@babel/types').V8IntrinsicIdentifier,\n  matcher: Matcher,\n): boolean {\n  // Traverse property accesses and function calls to find the leading ident\n  while (node.type === 'CallExpression' || node.type === 'MemberExpression') {\n    if (node.type === 'CallExpression') {\n      node = node.callee\n    } else if (node.type === 'MemberExpression') {\n      node = node.object\n    }\n  }\n\n  if (node.type === 'Identifier') {\n    return matcher.hasFunction(node.name)\n  }\n\n  return false\n}\n\nfunction canCollapseWhitespaceIn(\n  path: Path<import('@babel/types').Node, any>,\n  env: TransformerEnv,\n): false | { start: boolean; end: boolean } {\n  if (env.options.tailwindPreserveWhitespace) {\n    return false\n  }\n\n  let start = true\n  let end = true\n\n  for (let entry of path) {\n    if (!entry.parent) continue\n\n    // Nodes inside concat expressions shouldn't collapse whitespace\n    // depending on which side they're part of.\n    if (entry.parent.type === 'BinaryExpression' && entry.parent.operator === '+') {\n      start &&= entry.key !== 'right'\n      end &&= entry.key !== 'left'\n    }\n\n    // This is probably expression *inside* of a template literal. To collapse whitespace\n    // `Expression`s adjacent-before a quasi must start with whitespace\n    // `Expression`s adjacent-after a quasi must end with whitespace\n    //\n    // Note this check will bail out on more than it really should as it\n    // could be reset somewhere along the way by having whitespace around a\n    // string further up but not at the \"root\" but that complicates things\n    if (entry.parent.type === 'TemplateLiteral') {\n      let nodeStart = entry.node.start ?? null\n      let nodeEnd = entry.node.end ?? null\n\n      for (let quasi of entry.parent.quasis) {\n        let quasiStart = quasi.start ?? null\n        let quasiEnd = quasi.end ?? null\n\n        if (nodeStart !== null && quasiEnd !== null && nodeStart - quasiEnd <= 2) {\n          start &&= /^\\s/.test(quasi.value.raw)\n        }\n\n        if (nodeEnd !== null && quasiStart !== null && nodeEnd - quasiStart <= 2) {\n          end &&= /\\s$/.test(quasi.value.raw)\n        }\n      }\n    }\n  }\n\n  return { start, end }\n}\n\n// TODO: The `ast` types here aren't strictly correct.\n//\n// We cross several parsers that share roughly the same shape so things are\n// good enough. The actual AST we should be using is probably estree + ts.\nfunction transformJavaScript(ast: import('@babel/types').Node, env: TransformerEnv) {\n  let { matcher } = env\n\n  function sortInside(ast: import('@babel/types').Node) {\n    visit(ast, (node, path) => {\n      let collapseWhitespace = canCollapseWhitespaceIn(path, env)\n\n      if (isStringLiteral(node)) {\n        sortStringLiteral(node, { env, collapseWhitespace })\n      } else if (node.type === 'TemplateLiteral') {\n        sortTemplateLiteral(node, { env, collapseWhitespace })\n      } else if (node.type === 'TaggedTemplateExpression') {\n        if (isSortableTemplateExpression(node, matcher)) {\n          sortTemplateLiteral(node.quasi, { env, collapseWhitespace })\n        }\n      }\n    })\n  }\n\n  visit(ast, {\n    JSXAttribute(node) {\n      node = node as import('@babel/types').JSXAttribute\n\n      if (!node.value) {\n        return\n      }\n\n      // We don't want to support namespaced attributes (e.g. `somens:class`)\n      // React doesn't support them and most tools don't either\n      if (typeof node.name.name !== 'string') {\n        return\n      }\n\n      if (!matcher.hasStaticAttr(node.name.name)) {\n        return\n      }\n\n      if (isStringLiteral(node.value)) {\n        sortStringLiteral(node.value, { env })\n      } else if (node.value.type === 'JSXExpressionContainer') {\n        sortInside(node.value)\n      }\n    },\n\n    CallExpression(node) {\n      node = node as import('@babel/types').CallExpression\n\n      if (!isSortableCallExpression(node, matcher)) {\n        return\n      }\n\n      node.arguments.forEach((arg) => sortInside(arg))\n    },\n\n    TaggedTemplateExpression(node, path) {\n      node = node as import('@babel/types').TaggedTemplateExpression\n\n      if (!isSortableTemplateExpression(node, matcher)) {\n        return\n      }\n\n      let collapseWhitespace = canCollapseWhitespaceIn(path, env)\n\n      sortTemplateLiteral(node.quasi, {\n        env,\n        collapseWhitespace,\n      })\n    },\n  })\n}\n\nfunction transformCss(ast: any, env: TransformerEnv) {\n  // `parseValue` inside Prettier's CSS parser is private API so we have to\n  // produce the same result by parsing an import statement with the same params\n  function tryParseAtRuleParams(name: string, params: any) {\n    // It might already be an object or array. Could happen in the future if\n    // Prettier decides to start parsing these.\n    if (typeof params !== 'string') return params\n\n    // Otherwise we let prettier re-parse the params into its custom value AST\n    // based on postcss-value parser.\n    try {\n      let parser = prettierParserCss.parsers.css\n\n      let root = parser.parse(`@import ${params};`, {\n        // We can't pass env.options directly because css.parse overwrites\n        // options.originalText which is used during the printing phase\n        ...env.options,\n      })\n\n      return root.nodes[0].params\n    } catch (err) {\n      console.warn(`[prettier-plugin-tailwindcss] Unable to parse at rule`)\n      console.warn({ name, params })\n      console.warn(err)\n    }\n\n    return params\n  }\n\n  ast.walk((node: any) => {\n    if (node.name === 'plugin' || node.name === 'config' || node.name === 'source') {\n      node.params = tryParseAtRuleParams(node.name, node.params)\n    }\n\n    if (node.type === 'css-atrule' && node.name === 'apply') {\n      let isImportant = /\\s+(?:!important|#{(['\"]*)!important\\1})\\s*$/.test(node.params)\n\n      let classList = node.params\n\n      let prefix = ''\n      let suffix = ''\n\n      if (classList.startsWith('~\"') && classList.endsWith('\"')) {\n        prefix = '~\"'\n        suffix = '\"'\n        classList = classList.slice(2, -1)\n        isImportant = false\n      } else if (classList.startsWith(\"~'\") && classList.endsWith(\"'\")) {\n        prefix = \"~'\"\n        suffix = \"'\"\n        classList = classList.slice(2, -1)\n        isImportant = false\n      }\n\n      classList = sortClasses(classList, {\n        env,\n        ignoreLast: isImportant,\n        collapseWhitespace: {\n          start: false,\n          end: !isImportant,\n        },\n      })\n\n      node.params = `${prefix}${classList}${suffix}`\n    }\n  })\n}\n\nfunction transformAstro(ast: any, env: TransformerEnv) {\n  let { matcher } = env\n\n  if (ast.type === 'element' || ast.type === 'custom-element' || ast.type === 'component') {\n    for (let attr of ast.attributes ?? []) {\n      if (matcher.hasStaticAttr(attr.name) && attr.type === 'attribute' && attr.kind === 'quoted') {\n        attr.value = sortClasses(attr.value, {\n          env,\n        })\n      } else if (\n        matcher.hasDynamicAttr(attr.name) &&\n        attr.type === 'attribute' &&\n        attr.kind === 'expression' &&\n        typeof attr.value === 'string'\n      ) {\n        transformDynamicJsAttribute(attr, env)\n      }\n    }\n  }\n\n  for (let child of ast.children ?? []) {\n    transformAstro(child, env)\n  }\n}\n\nfunction transformMarko(ast: any, env: TransformerEnv) {\n  let { matcher } = env\n\n  const nodesToVisit = [ast]\n  while (nodesToVisit.length > 0) {\n    const currentNode = nodesToVisit.pop()\n    switch (currentNode.type) {\n      case 'File':\n        nodesToVisit.push(currentNode.program)\n        break\n      case 'Program':\n        nodesToVisit.push(...currentNode.body)\n        break\n      case 'MarkoTag':\n        nodesToVisit.push(...currentNode.attributes)\n        nodesToVisit.push(currentNode.body)\n        break\n      case 'MarkoTagBody':\n        nodesToVisit.push(...currentNode.body)\n        break\n      case 'MarkoAttribute':\n        if (!matcher.hasStaticAttr(currentNode.name)) break\n        switch (currentNode.value.type) {\n          case 'ArrayExpression':\n            const classList = currentNode.value.elements\n            for (const node of classList) {\n              if (node.type === 'StringLiteral') {\n                node.value = sortClasses(node.value, { env })\n              }\n            }\n            break\n          case 'StringLiteral':\n            currentNode.value.value = sortClasses(currentNode.value.value, {\n              env,\n            })\n            break\n        }\n        break\n    }\n  }\n}\n\nfunction transformTwig(ast: any, env: TransformerEnv) {\n  let { matcher } = env\n\n  for (let child of ast.expressions ?? []) {\n    transformTwig(child, env)\n  }\n\n  visit(ast, {\n    Attribute(node, _path, meta) {\n      if (!matcher.hasStaticAttr(node.name.name)) return\n\n      meta.sortTextNodes = true\n    },\n\n    CallExpression(node, _path, meta) {\n      // Traverse property accesses and function calls to find the *trailing* ident\n      while (node.type === 'CallExpression' || node.type === 'MemberExpression') {\n        if (node.type === 'CallExpression') {\n          node = node.callee\n        } else if (node.type === 'MemberExpression') {\n          // TODO: This is *different* than `isSortableExpression` and that doesn't feel right\n          // but they're mutually exclusive implementations\n          //\n          // This is to handle foo.fnNameHere(…) where `isSortableExpression` is intentionally\n          // handling `fnNameHere.foo(…)`.\n          node = node.property\n        }\n      }\n\n      if (node.type === 'Identifier') {\n        if (!matcher.hasFunction(node.name)) return\n      }\n\n      meta.sortTextNodes = true\n    },\n\n    StringLiteral(node, path, meta) {\n      if (!meta.sortTextNodes) {\n        return\n      }\n\n      const concat = path.find((entry) => {\n        return (\n          entry.parent &&\n          (entry.parent.type === 'BinaryConcatExpression' ||\n            entry.parent.type === 'BinaryAddExpression')\n        )\n      })\n\n      node.value = sortClasses(node.value, {\n        env,\n        ignoreFirst: concat?.key === 'right' && !/^[^\\S\\r\\n]/.test(node.value),\n        ignoreLast: concat?.key === 'left' && !/[^\\S\\r\\n]$/.test(node.value),\n        collapseWhitespace: {\n          start: concat?.key !== 'right',\n          end: concat?.key !== 'left',\n        },\n      })\n    },\n  })\n}\n\nfunction transformPug(ast: any, env: TransformerEnv) {\n  let { matcher } = env\n\n  // This isn't optimal\n  // We should merge the classes together across class attributes and class tokens\n  // And then we sort them\n  // But this is good enough for now\n\n  // First sort the classes in attributes\n  for (const token of ast.tokens) {\n    if (token.type === 'attribute' && matcher.hasStaticAttr(token.name)) {\n      token.val = [\n        token.val.slice(0, 1),\n        sortClasses(token.val.slice(1, -1), { env }),\n        token.val.slice(-1),\n      ].join('')\n    }\n  }\n\n  // Collect lists of consecutive class tokens\n  let startIdx = -1\n  let endIdx = -1\n  let ranges: [number, number][] = []\n\n  for (let i = 0; i < ast.tokens.length; i++) {\n    const token = ast.tokens[i]\n\n    if (token.type === 'class') {\n      startIdx = startIdx === -1 ? i : startIdx\n      endIdx = i\n    } else if (startIdx !== -1) {\n      ranges.push([startIdx, endIdx])\n      startIdx = -1\n      endIdx = -1\n    }\n  }\n\n  if (startIdx !== -1) {\n    ranges.push([startIdx, endIdx])\n    startIdx = -1\n    endIdx = -1\n  }\n\n  // Sort the lists of class tokens\n  for (const [startIdx, endIdx] of ranges) {\n    const classes = ast.tokens.slice(startIdx, endIdx + 1).map((token: any) => token.val)\n\n    const { classList } = sortClassList({\n      classList: classes,\n      api: env.context,\n      removeDuplicates: false,\n    })\n\n    for (let i = startIdx; i <= endIdx; i++) {\n      ast.tokens[i].val = classList[i - startIdx]\n    }\n  }\n}\n\nfunction transformSvelte(ast: any, env: TransformerEnv) {\n  let { matcher, changes } = env\n\n  for (let attr of ast.attributes ?? []) {\n    if (!matcher.hasStaticAttr(attr.name) || attr.type !== 'Attribute') {\n      continue\n    }\n\n    for (let i = 0; i < attr.value.length; i++) {\n      let value = attr.value[i]\n      if (value.type === 'Text') {\n        let same = value.raw === value.data\n        value.raw = sortClasses(value.raw, {\n          env,\n          ignoreFirst: i > 0 && !/^\\s/.test(value.raw),\n          ignoreLast: i < attr.value.length - 1 && !/\\s$/.test(value.raw),\n          removeDuplicates: true,\n          collapseWhitespace: false,\n        })\n        value.data = same\n          ? value.raw\n          : sortClasses(value.data, {\n              env,\n              ignoreFirst: i > 0 && !/^\\s/.test(value.data),\n              ignoreLast: i < attr.value.length - 1 && !/\\s$/.test(value.data),\n              removeDuplicates: true,\n              collapseWhitespace: false,\n            })\n      } else if (value.type === 'MustacheTag') {\n        visit(value.expression, {\n          Literal(node) {\n            if (isStringLiteral(node)) {\n              let before = node.raw\n              let sorted = sortStringLiteral(node, {\n                env,\n                removeDuplicates: false,\n                collapseWhitespace: false,\n              })\n\n              if (sorted) {\n                changes.push({\n                  before,\n                  after: node.raw,\n                  start: node.loc.start,\n                  end: node.loc.end,\n                })\n              }\n            }\n          },\n          TemplateLiteral(node) {\n            let before = node.quasis.map((quasi: any) => quasi.value.raw)\n            let sorted = sortTemplateLiteral(node, {\n              env,\n              removeDuplicates: false,\n              collapseWhitespace: false,\n            })\n\n            if (sorted) {\n              for (let [idx, quasi] of node.quasis.entries()) {\n                changes.push({\n                  before: before[idx],\n                  after: quasi.value.raw,\n                  start: quasi.loc.start,\n                  end: quasi.loc.end,\n                })\n              }\n            }\n          },\n        })\n      }\n    }\n  }\n\n  for (let child of ast.children ?? []) {\n    transformSvelte(child, env)\n  }\n\n  if (ast.type === 'IfBlock') {\n    for (let child of ast.else?.children ?? []) {\n      transformSvelte(child, env)\n    }\n  }\n\n  if (ast.type === 'AwaitBlock') {\n    let nodes = [ast.pending, ast.then, ast.catch]\n\n    for (let child of nodes) {\n      transformSvelte(child, env)\n    }\n  }\n\n  if (ast.html) {\n    transformSvelte(ast.html, env)\n  }\n}\n\nexport { options } from './options.js'\n\ntype HtmlNode =\n  | { type: 'attribute'; name: string; value: string }\n  | { kind: 'attribute'; name: string; value: string }\n\nlet html = defineTransform<HtmlNode>({\n  staticAttrs: ['class'],\n\n  load: [{ name: 'prettier/plugins/html', importer: () => import('prettier/plugins/html') }],\n  compatible: ['prettier-plugin-organize-attributes'],\n\n  parsers: {\n    html: {},\n    lwc: {},\n    angular: { dynamicAttrs: ['[ngClass]'] },\n    vue: { dynamicAttrs: [':class', 'v-bind:class'] },\n  },\n\n  transform: transformHtml,\n})\n\ntype GlimmerNode =\n  | { type: 'TextNode'; chars: string }\n  | { type: 'StringLiteral'; value: string }\n  | { type: 'ConcatStatement'; parts: GlimmerNode[] }\n  | { type: 'SubExpression'; path: { original: string } }\n  | { type: 'AttrNode'; name: string; value: GlimmerNode }\n\nlet glimmer = defineTransform<GlimmerNode>({\n  staticAttrs: ['class'],\n  load: [{ name: 'prettier/plugins/glimmer', importer: () => import('prettier/plugins/glimmer') }],\n\n  parsers: {\n    glimmer: {},\n  },\n\n  transform: transformGlimmer,\n})\n\ntype CssValueNode = { type: 'value-*'; name: string; params: string }\ntype CssNode = {\n  type: 'css-atrule'\n  name: string\n  params: string | CssValueNode\n}\n\nlet css = defineTransform<CssNode>({\n  load: [prettierParserCss],\n  compatible: ['prettier-plugin-css-order'],\n\n  parsers: {\n    css: {},\n    scss: {},\n    less: {},\n  },\n\n  transform: transformCss,\n})\n\nlet js = defineTransform<import('@babel/types').Node>({\n  staticAttrs: ['class', 'className'],\n  compatible: [\n    // The following plugins must come *before* the jsdoc plugin for it to\n    // function correctly. Additionally `multiline-arrays` usually needs to be\n    // placed before import sorting plugins.\n    //\n    // https://github.com/electrovir/prettier-plugin-multiline-arrays#compatibility\n    'prettier-plugin-multiline-arrays',\n    '@ianvs/prettier-plugin-sort-imports',\n    '@trivago/prettier-plugin-sort-imports',\n    'prettier-plugin-organize-imports',\n    'prettier-plugin-sort-imports',\n    'prettier-plugin-jsdoc',\n  ],\n\n  parsers: {\n    babel: { load: [prettierParserBabel] },\n    'babel-flow': { load: [prettierParserBabel] },\n    'babel-ts': { load: [prettierParserBabel] },\n    __js_expression: { load: [prettierParserBabel] },\n    typescript: {\n      load: [\n        {\n          name: 'prettier/plugins/typescript',\n          importer: () => import('prettier/plugins/typescript'),\n        },\n      ],\n    },\n    meriyah: {\n      load: [\n        { name: 'prettier/plugins/meriyah', importer: () => import('prettier/plugins/meriyah') },\n      ],\n    },\n    acorn: {\n      load: [{ name: 'prettier/plugins/acorn', importer: () => import('prettier/plugins/acorn') }],\n    },\n    flow: {\n      load: [{ name: 'prettier/plugins/flow', importer: () => import('prettier/plugins/flow') }],\n    },\n    oxc: {\n      load: [{ name: '@prettier/plugin-oxc', importer: () => import('@prettier/plugin-oxc') }],\n    },\n    'oxc-ts': {\n      load: [{ name: '@prettier/plugin-oxc', importer: () => import('@prettier/plugin-oxc') }],\n    },\n    hermes: {\n      load: [\n        { name: '@prettier/plugin-hermes', importer: () => import('@prettier/plugin-hermes') },\n      ],\n    },\n    astroExpressionParser: {\n      load: [\n        {\n          name: 'prettier-plugin-astro',\n          importer: () => {\n            // @ts-expect-error - This plugin doesn't have types\n            return import('prettier-plugin-astro')\n          },\n        },\n      ],\n      staticAttrs: ['class'],\n      dynamicAttrs: ['class:list'],\n    },\n  },\n\n  transform: transformJavaScript,\n})\n\ntype SvelteNode = import('svelte/compiler').AST.SvelteNode & {\n  changes: StringChange[]\n}\n\nlet svelte = defineTransform<SvelteNode>({\n  staticAttrs: ['class'],\n  load: [{ name: 'prettier-plugin-svelte', importer: () => import('prettier-plugin-svelte') }],\n\n  parsers: {\n    svelte: {},\n  },\n\n  printers: {\n    'svelte-ast': {},\n  },\n\n  transform: transformSvelte,\n\n  reprint(path, { options, changes }) {\n    if (options.__mutatedOriginalText) return\n    options.__mutatedOriginalText = true\n\n    if (!changes?.length) return\n\n    let finder = lineColumn(options.originalText)\n\n    let stringChanges: StringChange[] = changes.map((change) => ({\n      ...change,\n      start: finder.toIndex(change.start.line, change.start.column + 1),\n      end: finder.toIndex(change.end.line, change.end.column + 1),\n    }))\n\n    options.originalText = spliceChangesIntoString(options.originalText, stringChanges)\n  },\n})\n\ntype AstroNode =\n  | { type: 'element'; attributes: Extract<AstroNode, { type: 'attribute' }>[] }\n  | {\n      type: 'custom-element'\n      attributes: Extract<AstroNode, { type: 'attribute' }>[]\n    }\n  | {\n      type: 'component'\n      attributes: Extract<AstroNode, { type: 'attribute' }>[]\n    }\n  | { type: 'attribute'; kind: 'quoted'; name: string; value: string }\n  | { type: 'attribute'; kind: 'expression'; name: string; value: unknown }\n\nlet astro = defineTransform<AstroNode>({\n  staticAttrs: ['class', 'className'],\n  dynamicAttrs: ['class:list', 'className'],\n  load: [\n    {\n      name: 'prettier-plugin-astro',\n      importer: () => {\n        // @ts-expect-error - This plugin doesn't have types\n        return import('prettier-plugin-astro')\n      },\n    },\n  ],\n\n  parsers: {\n    astro: {},\n  },\n\n  transform: transformAstro,\n})\n\ntype MarkoNode = import('@marko/compiler').types.Node\n\nlet marko = defineTransform<MarkoNode>({\n  staticAttrs: ['class'],\n  load: [{ name: 'prettier-plugin-marko', importer: () => import('prettier-plugin-marko') }],\n\n  parsers: {\n    marko: {},\n  },\n\n  transform: transformMarko,\n})\n\ntype TwigIdentifier = { type: 'Identifier'; name: string }\n\ntype TwigMemberExpression = {\n  type: 'MemberExpression'\n  property: TwigIdentifier | TwigCallExpression | TwigMemberExpression\n}\n\ntype TwigCallExpression = {\n  type: 'CallExpression'\n  callee: TwigIdentifier | TwigCallExpression | TwigMemberExpression\n}\n\ntype TwigNode =\n  | { type: 'Attribute'; name: TwigIdentifier }\n  | { type: 'StringLiteral'; value: string }\n  | { type: 'BinaryConcatExpression' }\n  | { type: 'BinaryAddExpression' }\n  | TwigIdentifier\n  | TwigMemberExpression\n  | TwigCallExpression\n\nlet twig = defineTransform<TwigNode>({\n  staticAttrs: ['class'],\n  load: [\n    {\n      name: '@zackad/prettier-plugin-twig',\n      importer: () => {\n        // @ts-expect-error - This plugin doesn't have types\n        return import('@zackad/prettier-plugin-twig')\n      },\n    },\n  ],\n\n  parsers: {\n    twig: {},\n  },\n\n  transform: transformTwig,\n})\n\ninterface PugNode {\n  content: string\n  tokens: import('pug-lexer').Token[]\n}\n\nlet pug = defineTransform<PugNode>({\n  staticAttrs: ['class'],\n  load: [{ name: '@prettier/plugin-pug', importer: () => import('@prettier/plugin-pug') }],\n\n  parsers: {\n    pug: {},\n  },\n\n  transform: transformPug,\n})\n\ntype LiquidNode =\n  | Liquid.TextNode\n  | Liquid.AttributeNode\n  | Liquid.LiquidTag\n  | Liquid.HtmlElement\n  | Liquid.DocumentNode\n  | Liquid.LiquidExpression\n\nlet liquid = defineTransform<LiquidNode>({\n  staticAttrs: ['class'],\n  load: [\n    {\n      name: '@shopify/prettier-plugin-liquid',\n      importer: () => import('@shopify/prettier-plugin-liquid'),\n    },\n  ],\n\n  parsers: { 'liquid-html': {} },\n\n  transform: transformLiquid,\n})\n\nexport const { parsers, printers } = createPlugin([\n  //\n  html,\n  glimmer,\n  css,\n  js,\n  svelte,\n  astro,\n  marko,\n  twig,\n  pug,\n  liquid,\n])\n\nexport interface PluginOptions {\n  /**\n   * Path to the Tailwind config file.\n   */\n  tailwindConfig?: string\n\n  /**\n   * Path to the CSS stylesheet used by Tailwind CSS (v4+)\n   */\n  tailwindStylesheet?: string\n\n  /**\n   * Path to the CSS stylesheet used by Tailwind CSS (v4+)\n   *\n   * @deprecated Use `tailwindStylesheet` instead\n   */\n  tailwindEntryPoint?: string\n\n  /**\n   * List of custom function and tag names that contain classes.\n   *\n   * Default: []\n   */\n  tailwindFunctions?: string[]\n\n  /**\n   * List of custom attributes that contain classes.\n   *\n   * Default: []\n   */\n  tailwindAttributes?: string[]\n\n  /**\n   * Preserve whitespace around Tailwind classes when sorting.\n   *\n   * Default: false\n   */\n  tailwindPreserveWhitespace?: boolean\n\n  /**\n   * Preserve duplicate classes inside a class list when sorting.\n   *\n   * Default: false\n   */\n  tailwindPreserveDuplicates?: boolean\n}\n"
  },
  {
    "path": "src/internal.d.ts",
    "content": "import type { PluginOptions } from '.'\n\nexport interface InternalOptions extends PluginOptions {\n  printer: Printer<any>\n\n  /**\n   * The package name to use when loading Tailwind CSS\n   */\n  tailwindPackageName?: string\n}\n\nexport interface InternalPlugin {\n  name?: string\n}\n\ndeclare module 'prettier' {\n  interface RequiredOptions extends InternalOptions {}\n  interface ParserOptions extends InternalOptions {}\n  interface Plugin<T = any> extends InternalPlugin {}\n}\n"
  },
  {
    "path": "src/options.ts",
    "content": "import type { RequiredOptions, SupportOption } from 'prettier'\nimport type { Customizations } from './types'\n\nexport const options: Record<string, SupportOption> = {\n  tailwindConfig: {\n    type: 'string',\n    category: 'Tailwind CSS',\n    description: 'Path to Tailwind configuration file',\n  },\n\n  tailwindEntryPoint: {\n    type: 'string',\n    category: 'Tailwind CSS',\n    description: 'Path to the CSS entrypoint in your Tailwind project (v4+)',\n\n    // Can't include this otherwise the option is not passed to parsers\n    // deprecated: \"This option is deprecated. Use 'tailwindStylesheet' instead.\",\n  },\n\n  tailwindStylesheet: {\n    type: 'string',\n    category: 'Tailwind CSS',\n    description: 'Path to the CSS stylesheet in your Tailwind project (v4+)',\n  },\n\n  tailwindAttributes: {\n    type: 'string',\n    array: true,\n    default: [{ value: [] }],\n    category: 'Tailwind CSS',\n    description: 'List of attributes/props that contain sortable Tailwind classes',\n  },\n\n  tailwindFunctions: {\n    type: 'string',\n    array: true,\n    default: [{ value: [] }],\n    category: 'Tailwind CSS',\n    description: 'List of functions and tagged templates that contain sortable Tailwind classes',\n  },\n\n  tailwindPreserveWhitespace: {\n    type: 'boolean',\n    default: false,\n    category: 'Tailwind CSS',\n    description: 'Preserve whitespace around Tailwind classes when sorting',\n  },\n\n  tailwindPreserveDuplicates: {\n    type: 'boolean',\n    default: false,\n    category: 'Tailwind CSS',\n    description: 'Preserve duplicate classes inside a class list when sorting',\n  },\n\n  tailwindPackageName: {\n    type: 'string',\n    default: 'tailwindcss',\n    category: 'Tailwind CSS',\n    description: 'The package name to use when loading Tailwind CSS',\n  },\n}\n\nexport interface Matcher {\n  hasStaticAttr(name: string): boolean\n  hasDynamicAttr(name: string): boolean\n  hasFunction(name: string): boolean\n}\n\nexport function createMatcher(\n  options: RequiredOptions,\n  parser: string,\n  defaults: Customizations,\n): Matcher {\n  let staticAttrs = new Set<string>(defaults.staticAttrs)\n  let dynamicAttrs = new Set<string>(defaults.dynamicAttrs)\n  let functions = new Set<string>(defaults.functions)\n  let staticAttrsRegex: RegExp[] = [...defaults.staticAttrsRegex]\n  let functionsRegex: RegExp[] = [...defaults.functionsRegex]\n\n  // Create a list of \"static\" attributes\n  for (let attr of options.tailwindAttributes ?? []) {\n    let regex = parseRegex(attr)\n\n    if (regex) {\n      staticAttrsRegex.push(regex)\n    } else if (parser === 'vue' && attr.startsWith(':')) {\n      staticAttrs.add(attr.slice(1))\n    } else if (parser === 'vue' && attr.startsWith('v-bind:')) {\n      staticAttrs.add(attr.slice(7))\n    } else if (parser === 'vue' && attr.startsWith('v-')) {\n      dynamicAttrs.add(attr)\n    } else if (parser === 'angular' && attr.startsWith('[') && attr.endsWith(']')) {\n      staticAttrs.add(attr.slice(1, -1))\n    } else {\n      staticAttrs.add(attr)\n    }\n  }\n\n  // Generate a list of dynamic attributes\n  for (let attr of staticAttrs) {\n    if (parser === 'vue') {\n      dynamicAttrs.add(`:${attr}`)\n      dynamicAttrs.add(`v-bind:${attr}`)\n    } else if (parser === 'angular') {\n      dynamicAttrs.add(`[${attr}]`)\n    }\n  }\n\n  // Generate a list of supported functions\n  for (let fn of options.tailwindFunctions ?? []) {\n    let regex = parseRegex(fn)\n\n    if (regex) {\n      functionsRegex.push(regex)\n    } else {\n      functions.add(fn)\n    }\n  }\n\n  return {\n    hasStaticAttr: (name: string) => {\n      // If the name looks like a dynamic attribute we're not a static attr\n      // Only applies to Vue and Angular\n      let newName = nameFromDynamicAttr(name, parser)\n      if (newName) return false\n\n      return hasMatch(name, staticAttrs, staticAttrsRegex)\n    },\n\n    hasDynamicAttr: (name: string) => {\n      // This is definitely a dynamic attribute\n      if (hasMatch(name, dynamicAttrs, [])) return true\n\n      // If the name looks like a dynamic attribute compare the actual name\n      // Only applies to Vue and Angular\n      let newName = nameFromDynamicAttr(name, parser)\n      if (!newName) return false\n\n      return hasMatch(newName, staticAttrs, staticAttrsRegex)\n    },\n\n    hasFunction: (name: string) => hasMatch(name, functions, functionsRegex),\n  }\n}\n\nfunction nameFromDynamicAttr(name: string, parser: string) {\n  if (parser === 'vue') {\n    if (name.startsWith(':')) return name.slice(1)\n    if (name.startsWith('v-bind:')) return name.slice(7)\n    if (name.startsWith('v-')) return name\n    return null\n  }\n\n  if (parser === 'angular') {\n    if (name.startsWith('[') && name.endsWith(']')) return name.slice(1, -1)\n    return null\n  }\n\n  return null\n}\n\n/**\n * Check for matches against a static list or possible regex patterns\n */\nfunction hasMatch(name: string, list: Set<string>, patterns: RegExp[]): boolean {\n  if (list.has(name)) return true\n\n  for (let regex of patterns) {\n    if (regex.test(name)) return true\n  }\n\n  return false\n}\n\nfunction parseRegex(str: string): RegExp | null {\n  if (!str.startsWith('/')) return null\n\n  let lastSlash = str.lastIndexOf('/')\n  if (lastSlash <= 0) return null\n\n  try {\n    let pattern = str.slice(1, lastSlash)\n    let flags = str.slice(lastSlash + 1)\n    return new RegExp(pattern, flags)\n  } catch {\n    return null\n  }\n}\n"
  },
  {
    "path": "src/resolve.ts",
    "content": "import fs from 'node:fs'\nimport { fileURLToPath } from 'node:url'\nimport { CachedInputFileSystem, ResolverFactory } from 'enhanced-resolve'\nimport { expiringMap } from './expiring-map'\n\nconst fileSystem = new CachedInputFileSystem(fs, 30_000)\n\nconst esmResolver = ResolverFactory.createResolver({\n  fileSystem,\n  useSyncFileSystemCalls: true,\n  extensions: ['.mjs', '.js'],\n  mainFields: ['module'],\n  conditionNames: ['node', 'import'],\n})\n\nconst cjsResolver = ResolverFactory.createResolver({\n  fileSystem,\n  useSyncFileSystemCalls: true,\n  extensions: ['.js', '.cjs'],\n  mainFields: ['main'],\n  conditionNames: ['node', 'require'],\n})\n\nconst cssResolver = ResolverFactory.createResolver({\n  fileSystem,\n  useSyncFileSystemCalls: true,\n  extensions: ['.css'],\n  mainFields: ['style'],\n  conditionNames: ['style'],\n})\n\n// This is a long-lived cache for resolved modules whether they exist or not\n// Because we're compatible with a large number of plugins, we need to check\n// for the existence of a module before attempting to import it. This cache\n// is used to mitigate the cost of that check because Node.js does not cache\n// failed module resolutions making repeated checks very expensive.\nconst resolveCache = expiringMap<string, string | null>(30_000)\n\nexport function maybeResolve(name: string) {\n  let modpath = resolveCache.get(name)\n\n  if (modpath === undefined) {\n    try {\n      modpath = resolveJsFrom(fileURLToPath(import.meta.url), name)\n      resolveCache.set(name, modpath)\n    } catch {\n      resolveCache.set(name, null)\n      return null\n    }\n  }\n\n  return modpath\n}\n\nexport async function loadIfExists<T>(name: string): Promise<T | null> {\n  let modpath = maybeResolve(name)\n\n  if (modpath) {\n    let mod = await import(name)\n    return mod.default ?? mod\n  }\n\n  return null\n}\n\nexport function resolveJsFrom(base: string, id: string): string {\n  try {\n    return esmResolver.resolveSync({}, base, id) || id\n  } catch {\n    return cjsResolver.resolveSync({}, base, id) || id\n  }\n}\n\nexport function resolveCssFrom(base: string, id: string) {\n  return cssResolver.resolveSync({}, base, id) || id\n}\n"
  },
  {
    "path": "src/sorter.ts",
    "content": "import * as path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport escalade from 'escalade/sync'\nimport * as console from './console'\nimport { expiringMap } from './expiring-map.js'\nimport { resolveJsFrom } from './resolve'\nimport { sortClasses, sortClassList } from './sorting.js'\nimport type { TransformerEnv, UnifiedApi } from './types'\nimport { cacheForDirs } from './utils.js'\n\nexport interface SorterOptions {\n  /**\n   * The directory used to resolve relative file paths.\n   *\n   * When not provided this will be:\n   * - The current working directory\n   */\n  base?: string\n\n  /**\n   * The path to the file being formatted.\n   *\n   * When provided, Tailwind CSS is resolved relative to this path; otherwise,\n   * it is resolved relative to `base`.\n   */\n  filepath?: string\n\n  /**\n   * Path to the Tailwind CSS config file (v3).\n   *\n   * Paths are resolved relative to `base`.\n   */\n  configPath?: string\n\n  /**\n   * Path to the CSS stylesheet used by Tailwind CSS (v4+).\n   *\n   * Paths are resolved relative to `base`.\n   */\n  stylesheetPath?: string\n\n  /**\n   * Whether or not to preserve whitespace around classes.\n   *\n   * Default: false\n   */\n  preserveWhitespace?: boolean\n\n  /**\n   * Whether or not to preserve duplicate classes.\n   *\n   * Default: false\n   */\n  preserveDuplicates?: boolean\n\n  /**\n   * The package name to use when loading Tailwind CSS.\n   *\n   * Useful when multiple versions are installed in the same project.\n   *\n   * Default: `tailwindcss`\n   *\n   * @internal\n   */\n  packageName?: string\n}\n\nexport interface Sorter {\n  /**\n   * Sort one or more class attributes.\n   *\n   * Each element is the value of an HTML `class` attribute (or similar). i.e. a\n   * space separated list of class names as a string.\n   */\n  sortClassAttributes(classes: string[]): string[]\n\n  /**\n   * Sort one or more class lists.\n   *\n   * Each element is an array of class names. Passing a space separated class\n   * list in each element is not supported.\n   *\n   * Duplicates are removed by default unless `preserveDuplicates` is enabled.\n   */\n  sortClassLists(classes: string[][]): string[][]\n}\n\ntype TailwindConfigOptions = {\n  base?: string\n  filepath?: string\n  configPath?: string\n  stylesheetPath?: string\n  packageName?: string\n}\n\nfunction resolveIfRelative(base: string, filePath?: string) {\n  if (!filePath) return null\n  return path.isAbsolute(filePath) ? filePath : path.resolve(base, filePath)\n}\n\nlet pathToApiMap = expiringMap<string | null, Promise<UnifiedApi>>(10_000)\n\n/**\n * Get a Tailwind CSS API instance based on the provided options.\n * @internal\n */\nexport async function getTailwindConfig(options: TailwindConfigOptions): Promise<UnifiedApi> {\n  let base = options.base ?? process.cwd()\n  let inputDir = options.filepath ? path.dirname(options.filepath) : base\n\n  let configPath = resolveIfRelative(base, options.configPath)\n  let stylesheetPath = resolveIfRelative(base, options.stylesheetPath)\n\n  // Locate Tailwind CSS itself\n  //\n  // We resolve this like we're in `inputDir` for better monorepo support as\n  // Prettier may be configured at the workspace root but Tailwind CSS is\n  // installed for a workspace package rather than the entire monorepo\n  let [mod, pkgDir] = await resolveTailwindPath({ packageName: options.packageName }, inputDir)\n\n  // Locate project stylesheet relative to the formatter config file\n  //\n  // We resolve this relative to the config file because it is *required*\n  // to work with a project's custom config. Given that, resolving it\n  // relative to where the path is defined makes the most sense.\n  let stylesheet = resolveStylesheet(stylesheetPath, base)\n\n  // Locate *explicit* v3 configs relative to the formatter config file\n  //\n  // We use this as a signal that we should always use v3 to format files even\n  // when the local install is v4 — which means we'll use the bundled v3.\n  let jsConfig = resolveJsConfigPath(configPath)\n\n  // Locate the closest v3 config file\n  //\n  // Note:\n  // We only need to do this when a stylesheet has not been provided otherwise\n  // we'd know for sure this was a v4 project regardless of what local Tailwind\n  // CSS installation is present. Additionally, if the local version is v4 we\n  // skip this as we assume that the user intends to use that version.\n  //\n  // The config path is resolved in one of two ways:\n  //\n  // 1. When automatic, relative to the input file\n  //\n  // This ensures monorepos can load the \"closest\" JS config for a given file\n  // which is important when a workspace package includes Tailwind CSS *and*\n  // Prettier is configured globally instead of per-package.\n  //\n  // 2. When explicit via `configPath`, relative to `base`\n  if (!stylesheet && !mod?.__unstable__loadDesignSystem) {\n    jsConfig = jsConfig ?? findClosestJsConfig(inputDir)\n  }\n\n  // We've found a JS config either because it was specified by the user\n  // or because it was automatically located. This means we should use v3.\n  if (jsConfig) {\n    if (!stylesheet) {\n      return pathToApiMap.remember(`${pkgDir}:${jsConfig}`, async () => {\n        const { loadV3 } = await import('./versions/v3')\n        return loadV3(pkgDir, jsConfig)\n      })\n    }\n\n    // In this case the user explicitly gave us a stylesheet and a config.\n    // Warn them about this and use the bundled v4.\n    console.error(\n      'explicit-stylesheet-and-config-together',\n      base,\n      `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.`,\n    )\n  }\n\n  if (mod && !mod.__unstable__loadDesignSystem) {\n    if (!stylesheet) {\n      return pathToApiMap.remember(`${pkgDir}:${jsConfig}`, async () => {\n        const { loadV3 } = await import('./versions/v3')\n        return loadV3(pkgDir, jsConfig)\n      })\n    }\n\n    // In this case the user explicitly gave us a stylesheet but their local\n    // installation is not v4. We'll fallback to the bundled v4 in this case.\n    mod = null\n    console.error(\n      'stylesheet-unsupported',\n      base,\n      'You have specified a Tailwind CSS stylesheet but your installed version of Tailwind CSS does not support this feature.',\n    )\n  }\n\n  // If we've detected a local version of v4 then we should fallback to using\n  // its included theme as the stylesheet if the user didn't give us one.\n  if (mod && mod.__unstable__loadDesignSystem && pkgDir) {\n    stylesheet ??= `${pkgDir}/theme.css`\n  }\n\n  return pathToApiMap.remember(`${pkgDir}:${stylesheet}`, async () => {\n    const { loadV4 } = await import('./versions/v4')\n    return loadV4(mod, stylesheet)\n  })\n}\n\nlet resolvedModCache = expiringMap<string, [any, string | null]>(10_000)\n\nasync function resolveTailwindPath(\n  options: { packageName?: string },\n  baseDir: string,\n): Promise<[any, string | null]> {\n  let pkgName = options.packageName ?? 'tailwindcss'\n  let makeKey = (dir: string) => `${pkgName}:${dir}`\n\n  // Check cache for this directory\n  let cached = resolvedModCache.get(makeKey(baseDir))\n  if (cached !== undefined) {\n    return cached\n  }\n\n  let resolve = async () => {\n    let pkgDir: string | null = null\n    let mod: any = null\n\n    try {\n      let pkgPath = resolveJsFrom(baseDir, pkgName)\n      mod = await import(pathToFileURL(pkgPath).toString())\n\n      let pkgFile = resolveJsFrom(baseDir, `${pkgName}/package.json`)\n      pkgDir = path.dirname(pkgFile)\n    } catch {}\n\n    return [mod, pkgDir] as [any, string | null]\n  }\n\n  let result = await resolve()\n\n  // Cache all directories from baseDir up to package location\n  let [, pkgDir] = result\n  if (pkgDir) {\n    cacheForDirs(resolvedModCache, baseDir, result, pkgDir, makeKey)\n  } else {\n    resolvedModCache.set(makeKey(baseDir), result)\n  }\n\n  return result\n}\n\nfunction resolveJsConfigPath(configPath: string | null): string | null {\n  if (!configPath) return null\n  if (configPath.endsWith('.css')) return null\n  return configPath\n}\n\nlet configPathCache = new Map<string, string | null>()\n\nfunction findClosestJsConfig(inputDir: string): string | null {\n  // Check cache for this directory\n  let cached = configPathCache.get(inputDir)\n  if (cached !== undefined) {\n    return cached\n  }\n\n  // Resolve\n  let configPath: string | null = null\n  try {\n    let foundPath = escalade(inputDir, (_, names) => {\n      if (names.includes('tailwind.config.js')) return 'tailwind.config.js'\n      if (names.includes('tailwind.config.cjs')) return 'tailwind.config.cjs'\n      if (names.includes('tailwind.config.mjs')) return 'tailwind.config.mjs'\n      if (names.includes('tailwind.config.ts')) return 'tailwind.config.ts'\n    })\n    configPath = foundPath ?? null\n  } catch {}\n\n  // Cache all directories from inputDir up to config location\n  if (configPath) {\n    cacheForDirs(configPathCache, inputDir, configPath, path.dirname(configPath))\n  } else {\n    configPathCache.set(inputDir, null)\n  }\n\n  return configPath\n}\n\nfunction resolveStylesheet(stylesheetPath: string | null, base: string): string | null {\n  if (!stylesheetPath) return null\n\n  if (\n    stylesheetPath.endsWith('.js') ||\n    stylesheetPath.endsWith('.mjs') ||\n    stylesheetPath.endsWith('.cjs') ||\n    stylesheetPath.endsWith('.ts') ||\n    stylesheetPath.endsWith('.mts') ||\n    stylesheetPath.endsWith('.cts')\n  ) {\n    console.error(\n      'stylesheet-is-js-file',\n      base,\n      \"Your `stylesheetPath` option points to a JS/TS config file. You must point to your project's `.css` file for v4 projects.\",\n    )\n  } else if (\n    stylesheetPath.endsWith('.sass') ||\n    stylesheetPath.endsWith('.scss') ||\n    stylesheetPath.endsWith('.less') ||\n    stylesheetPath.endsWith('.styl')\n  ) {\n    console.error(\n      'stylesheet-is-preprocessor-file',\n      base,\n      'Your `stylesheetPath` option points to a preprocessor file. This is unsupported and you may get unexpected results.',\n    )\n  } else if (!stylesheetPath.endsWith('.css')) {\n    console.error(\n      'stylesheet-is-not-css-file',\n      base,\n      'Your `stylesheetPath` option does not point to a CSS file. This is unsupported and you may get unexpected results.',\n    )\n  }\n\n  return stylesheetPath\n}\n\n/**\n * Creates a sorter instance for sorting Tailwind CSS classes.\n *\n * This function initializes a sorter with the specified Tailwind CSS configuration.\n * The sorter can be used to sort class attributes (space-separated strings) or\n * class lists (arrays of class names).\n\n * @example\n * ```ts\n * const sorter = await createSorter({})\n *\n * // Sort class lists\n * const sorted = sorter.sortClassLists([['p-4', 'm-2']])\n * // Returns: [['m-2', 'p-4']]\n * ```\n */\nexport async function createSorter(opts: SorterOptions): Promise<Sorter> {\n  let preserveDuplicates = opts.preserveDuplicates ?? false\n  let preserveWhitespace = opts.preserveWhitespace ?? false\n\n  let api = await getTailwindConfig({\n    base: opts.base,\n    filepath: opts.filepath,\n    configPath: opts.configPath,\n    stylesheetPath: opts.stylesheetPath,\n    packageName: opts.packageName,\n  })\n\n  let env: TransformerEnv = {\n    context: api,\n    changes: [],\n    options: {\n      tailwindPreserveWhitespace: preserveWhitespace,\n      tailwindPreserveDuplicates: preserveDuplicates,\n      tailwindPackageName: opts.packageName,\n    } as any,\n    matcher: undefined as any,\n  }\n\n  return {\n    sortClassLists(classes) {\n      return classes.map((list) => {\n        let result = sortClassList({\n          api,\n          classList: list,\n          removeDuplicates: !preserveDuplicates,\n        })\n\n        return result.classList\n      })\n    },\n\n    sortClassAttributes(classes) {\n      return classes.map((list) => sortClasses(list, { env }))\n    },\n  }\n}\n"
  },
  {
    "path": "src/sorting.ts",
    "content": "import type { TransformerEnv, UnifiedApi } from './types'\nimport { bigSign } from './utils'\n\nexport interface SortOptions {\n  ignoreFirst?: boolean\n  ignoreLast?: boolean\n  removeDuplicates?: boolean\n  collapseWhitespace?: false | { start: boolean; end: boolean }\n}\n\nexport function sortClasses(\n  classStr: string,\n  {\n    env,\n    ignoreFirst = false,\n    ignoreLast = false,\n    removeDuplicates = true,\n    collapseWhitespace = { start: true, end: true },\n  }: SortOptions & {\n    env: TransformerEnv\n  },\n): string {\n  if (typeof classStr !== 'string' || classStr === '') {\n    return classStr\n  }\n\n  // Ignore class attributes containing `{{`, to match Prettier behaviour:\n  // https://github.com/prettier/prettier/blob/8a88cdce6d4605f206305ebb9204a0cabf96a070/src/language-html/embed/class-names.js#L9\n  if (classStr.includes('{{')) {\n    return classStr\n  }\n\n  if (env.options.tailwindPreserveWhitespace) {\n    collapseWhitespace = false\n  }\n\n  if (env.options.tailwindPreserveDuplicates) {\n    removeDuplicates = false\n  }\n\n  // This class list is purely whitespace\n  // Collapse it to a single space if the option is enabled\n  if (collapseWhitespace && /^[\\t\\r\\f\\n ]+$/.test(classStr)) {\n    return ' '\n  }\n\n  let result = ''\n  let parts = classStr.split(/([\\t\\r\\f\\n ]+)/)\n  let classes = parts.filter((_, i) => i % 2 === 0)\n  let whitespace = parts.filter((_, i) => i % 2 !== 0)\n\n  if (classes[classes.length - 1] === '') {\n    classes.pop()\n  }\n\n  if (collapseWhitespace) {\n    whitespace = whitespace.map(() => ' ')\n  }\n\n  let prefix = ''\n  if (ignoreFirst) {\n    prefix = `${classes.shift() ?? ''}${whitespace.shift() ?? ''}`\n  }\n\n  let suffix = ''\n  if (ignoreLast) {\n    suffix = `${whitespace.pop() ?? ''}${classes.pop() ?? ''}`\n  }\n\n  let { classList, removedIndices } = sortClassList({\n    classList: classes,\n    api: env.context,\n    removeDuplicates,\n  })\n\n  // Remove whitespace that appeared before a removed classes\n  whitespace = whitespace.filter((_, index) => !removedIndices.has(index + 1))\n\n  for (let i = 0; i < classList.length; i++) {\n    result += `${classList[i]}${whitespace[i] ?? ''}`\n  }\n\n  if (collapseWhitespace) {\n    prefix = prefix.replace(/\\s+$/g, ' ')\n    suffix = suffix.replace(/^\\s+/g, ' ')\n\n    result = result\n      .replace(/^\\s+/, collapseWhitespace.start ? '' : ' ')\n      .replace(/\\s+$/, collapseWhitespace.end ? '' : ' ')\n  }\n\n  return prefix + result + suffix\n}\n\nexport function sortClassList({\n  classList,\n  api,\n  removeDuplicates,\n}: {\n  classList: string[]\n  api: UnifiedApi\n  removeDuplicates: boolean\n}) {\n  // Re-order classes based on the Tailwind CSS configuration\n  let orderedClasses = api.getClassOrder(classList)\n\n  orderedClasses.sort(([nameA, a], [nameZ, z]) => {\n    // Move `...` to the end of the list\n    if (nameA === '...' || nameA === '…') return 1\n    if (nameZ === '...' || nameZ === '…') return -1\n\n    if (a === z) return 0\n    if (a === null) return -1\n    if (z === null) return 1\n    return bigSign(a - z)\n  })\n\n  // Remove duplicate Tailwind classes\n  let removedIndices = new Set<number>()\n\n  if (removeDuplicates) {\n    let seenClasses = new Set<string>()\n\n    orderedClasses = orderedClasses.filter(([cls, order], index) => {\n      if (seenClasses.has(cls)) {\n        removedIndices.add(index)\n        return false\n      }\n\n      // Only consider known classes when removing duplicates\n      if (order !== null) {\n        seenClasses.add(cls)\n      }\n\n      return true\n    })\n  }\n\n  return {\n    classList: orderedClasses.map(([className]) => className),\n    removedIndices,\n  }\n}\n"
  },
  {
    "path": "src/transform.ts",
    "content": "import type { AstPath, Plugin } from 'prettier'\nimport type { TransformerEnv } from './types'\n\nexport function defineTransform<T>(opts: TransformOptions<T>) {\n  return opts\n}\n\nexport interface LazyPluginLoad {\n  name: string\n  importer: () => Promise<unknown>\n}\n\nexport type PluginLoad = LazyPluginLoad | Plugin<any>\n\nexport interface TransformOptions<T> {\n  /**\n   * Static attributes that are supported by default\n   */\n  staticAttrs?: string[]\n\n  /**\n   * Dynamic / expression attributes that are supported by default\n   */\n  dynamicAttrs?: string[]\n\n  /**\n   * Load the given plugins for the parsers and printers\n   */\n  load?: PluginLoad[]\n\n  /**\n   * A list of compatible, third-party plugins for this transformation step\n   *\n   * The loading of these is delayed until the actual parse call as\n   * using the parse() function from these plugins may cause errors\n   * if they haven't already been loaded by Prettier.\n   */\n  compatible?: string[]\n\n  /**\n   * A list of supported parser names\n   */\n  parsers: Record<\n    string,\n    {\n      /**\n       * Load the given plugins for the parsers and printers\n       */\n      load?: PluginLoad[]\n\n      /**\n       * Static attributes that are supported by default\n       */\n      staticAttrs?: string[]\n\n      /**\n       * Dynamic / expression attributes that are supported by default\n       */\n      dynamicAttrs?: string[]\n    }\n  >\n\n  /**\n   * A list of supported parser names\n   */\n  printers?: Record<string, {}>\n\n  /**\n   * Transform entire ASTs\n   *\n   * @param ast  The AST to transform\n   * @param env  Provides options and mechanisms to sort classes\n   */\n  transform?(ast: T, env: TransformerEnv): void\n\n  /**\n   * Transform entire ASTs\n   *\n   * @param ast  The AST to transform\n   * @param env  Provides options and mechanisms to sort classes\n   */\n  reprint?(path: AstPath<T>, options: TransformerEnv): void\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "import type { ParserOptions } from 'prettier'\nimport type { Matcher } from './options'\n\nexport interface TransformerMetadata {\n  // Default customizations for a given transformer\n  functions?: string[]\n  staticAttrs?: string[]\n  dynamicAttrs?: string[]\n}\n\nexport interface Customizations {\n  functions: Set<string>\n  staticAttrs: Set<string>\n  dynamicAttrs: Set<string>\n  staticAttrsRegex: RegExp[]\n  dynamicAttrsRegex: RegExp[]\n  functionsRegex: RegExp[]\n}\n\nexport interface UnifiedApi {\n  getClassOrder(classList: string[]): [string, bigint | null][]\n}\n\nexport interface TransformerEnv {\n  context: UnifiedApi\n  matcher: Matcher\n  options: ParserOptions\n  changes: StringChangePositional[]\n}\n\nexport interface StringChangePositional {\n  start: { line: number; column: number }\n  end: { line: number; column: number }\n  before: string\n  after: string\n}\n\nexport interface StringChange {\n  start: number\n  end: number\n  before: string\n  after: string\n}\n"
  },
  {
    "path": "src/utils.bench.ts",
    "content": "import { bench, describe } from 'vitest'\nimport type { StringChange } from './types'\nimport { spliceChangesIntoString } from './utils'\n\ndescribe('spliceChangesIntoString', () => {\n  // 44 bytes\n  let strTemplate = 'the quick brown fox jumps over the lazy dog '\n  let changesTemplate: StringChange[] = [\n    { start: 10, end: 15, before: 'brown', after: 'purple' },\n    { start: 4, end: 9, before: 'quick', after: 'slow' },\n  ]\n\n  function buildFixture(repeatCount: number, changeCount: number) {\n    // A large set of changes across random places in the string\n    let indxes = new Set(\n      Array.from({ length: changeCount }, () => Math.ceil(Math.random() * repeatCount)),\n    )\n\n    let changes: StringChange[] = Array.from(indxes).flatMap((idx) => {\n      return changesTemplate.map((change) => ({\n        start: change.start + strTemplate.length * idx,\n        end: change.end + strTemplate.length * idx,\n        before: change.before,\n        after: change.after,\n      }))\n    })\n\n    return [strTemplate.repeat(repeatCount), changes] as const\n  }\n\n  let [strS, changesS] = buildFixture(5, 2)\n  bench('small string', () => {\n    spliceChangesIntoString(strS, changesS)\n  })\n\n  let [strM, changesM] = buildFixture(100, 5)\n  bench('medium string', () => {\n    spliceChangesIntoString(strM, changesM)\n  })\n\n  let [strL, changesL] = buildFixture(1_000, 50)\n  bench('large string', () => {\n    spliceChangesIntoString(strL, changesL)\n  })\n\n  let [strXL, changesXL] = buildFixture(100_000, 500)\n  bench('extra large string', () => {\n    spliceChangesIntoString(strXL, changesXL)\n  })\n\n  let [strXL2, changesXL2] = buildFixture(100_000, 5_000)\n  bench('extra large string (5k changes)', () => {\n    spliceChangesIntoString(strXL2, changesXL2)\n  })\n})\n"
  },
  {
    "path": "src/utils.test.ts",
    "content": "import { describe, test } from 'vitest'\nimport type { StringChange } from './types'\nimport { spliceChangesIntoString } from './utils'\n\ndescribe('spliceChangesIntoString', () => {\n  test('can apply changes to a string', ({ expect }) => {\n    let str = 'the quick brown fox jumps over the lazy dog'\n    let changes: StringChange[] = [\n      //\n      { start: 10, end: 15, before: 'brown', after: 'purple' },\n    ]\n\n    expect(spliceChangesIntoString(str, changes)).toBe(\n      'the quick purple fox jumps over the lazy dog',\n    )\n  })\n\n  test('changes are applied in order', ({ expect }) => {\n    let str = 'the quick brown fox jumps over the lazy dog'\n    let changes: StringChange[] = [\n      //\n      { start: 10, end: 15, before: 'brown', after: 'purple' },\n      { start: 4, end: 9, before: 'quick', after: 'slow' },\n    ]\n\n    expect(spliceChangesIntoString(str, changes)).toBe(\n      'the slow purple fox jumps over the lazy dog',\n    )\n  })\n})\n"
  },
  {
    "path": "src/utils.ts",
    "content": "import * as path from 'node:path'\nimport type { StringChange } from './types'\n\n// For loading prettier plugins only if they exist\nexport function loadIfExists(name: string): any {\n  try {\n    if (require.resolve(name)) {\n      return require(name)\n    }\n  } catch {\n    return null\n  }\n}\n\ninterface PathEntry<T, Meta> {\n  node: T\n  parent: T | null\n  key: string | null\n  index: number | null\n  meta: Meta\n}\n\nexport type Path<T, Meta> = PathEntry<T, Meta>[]\n\ntype Visitor<T, Meta extends Record<string, unknown>> = (\n  node: T,\n  path: Path<T, Meta>,\n  meta: Partial<Meta>,\n) => void | false\n\ntype Visitors<T, Meta extends Record<string, unknown>> = Record<string, Visitor<T, Meta>>\n\nfunction isNodeLike(value: any): value is { type: string } {\n  return typeof value?.type === 'string'\n}\n\n// https://lihautan.com/manipulating-ast-with-javascript/\nexport function visit<T extends {}, Meta extends Record<string, unknown>>(\n  ast: T,\n  callbackMap: Visitors<T, Meta> | Visitor<T, Meta>,\n) {\n  function _visit(node: any, path: Path<T, Meta>, meta: Meta) {\n    if (typeof callbackMap === 'function') {\n      if (callbackMap(node, path, meta) === false) {\n        return\n      }\n    } else if (node.type in callbackMap) {\n      if (callbackMap[node.type](node, path, meta) === false) {\n        return\n      }\n    }\n\n    const keys = Object.keys(node)\n    for (let i = 0; i < keys.length; i++) {\n      const child = node[keys[i]]\n      if (Array.isArray(child)) {\n        for (let j = 0; j < child.length; j++) {\n          if (isNodeLike(child[j])) {\n            let newMeta = { ...meta }\n            let newPath = [\n              {\n                node: child[j],\n                parent: node,\n                key: keys[i],\n                index: j,\n                meta: newMeta,\n              },\n              ...path,\n            ]\n\n            _visit(child[j], newPath, newMeta)\n          }\n        }\n      } else if (isNodeLike(child)) {\n        let newMeta = { ...meta }\n        let newPath = [\n          {\n            node: child,\n            parent: node,\n            key: keys[i],\n            index: i,\n            meta: newMeta,\n          },\n          ...path,\n        ]\n\n        _visit(child, newPath, newMeta)\n      }\n    }\n  }\n\n  let newMeta: Meta = {} as any\n  let newPath: Path<T, Meta> = [\n    {\n      node: ast,\n      parent: null,\n      key: null,\n      index: null,\n      meta: newMeta,\n    },\n  ]\n\n  _visit(ast, newPath, newMeta)\n}\n\n/**\n * Apply the changes to the string such that a change in the length\n * of the string does not break the indexes of the subsequent changes.\n */\nexport function spliceChangesIntoString(str: string, changes: StringChange[]) {\n  // If there are no changes, return the original string\n  if (!changes[0]) return str\n\n  // Sort all changes in order to make it easier to apply them\n  changes.sort((a, b) => {\n    return a.end - b.end || a.start - b.start\n  })\n\n  // Append original string between each chunk, and then the chunk itself\n  // This is sort of a String Builder pattern, thus creating less memory pressure\n  let result = ''\n\n  let previous = changes[0]\n\n  result += str.slice(0, previous.start)\n  result += previous.after\n\n  for (let i = 1; i < changes.length; ++i) {\n    let change = changes[i]\n\n    result += str.slice(previous.end, change.start)\n    result += change.after\n\n    previous = change\n  }\n\n  // Add leftover string from last chunk to end\n  result += str.slice(previous.end)\n\n  return result\n}\n\nexport function bigSign(bigIntValue: bigint) {\n  return Number(bigIntValue > 0n) - Number(bigIntValue < 0n)\n}\n\n/**\n * Cache a value for all directories from `inputDir` up to `targetDir` (inclusive).\n * Stops early if an existing cache entry is found.\n *\n * How it works:\n *\n * For a file at '/repo/packages/ui/src/Button.tsx' with config at '/repo/package.json'\n *\n * `cacheForDirs(cache, '/repo/packages/ui/src', '/repo/package.json', '/repo')`\n *\n * Caches:\n * - '/repo/packages/ui/src' -> '/repo/package.json'\n * - '/repo/packages/ui'     -> '/repo/package.json'\n * - '/repo/packages'        -> '/repo/package.json'\n * - '/repo'                 -> '/repo/package.json'\n */\nexport function cacheForDirs<V>(\n  cache: { set(key: string, value: V): void; get(key: string): V | undefined },\n  inputDir: string,\n  value: V,\n  targetDir: string,\n  makeKey: (dir: string) => string = (dir) => dir,\n): void {\n  let dir = inputDir\n  while (dir !== path.dirname(dir) && dir.length >= targetDir.length) {\n    const key = makeKey(dir)\n    // Stop caching if we hit an existing entry\n    if (cache.get(key) !== undefined) break\n\n    cache.set(key, value)\n    if (dir === targetDir) break\n    dir = path.dirname(dir)\n  }\n}\n"
  },
  {
    "path": "src/versions/assets.ts",
    "content": "// @ts-ignore\nimport index from 'tailwindcss-v4/index.css'\n// @ts-ignore\nimport preflight from 'tailwindcss-v4/preflight.css'\n// @ts-ignore\nimport theme from 'tailwindcss-v4/theme.css'\n// @ts-ignore\nimport utilities from 'tailwindcss-v4/utilities.css'\n\nexport const assets: Record<string, string> = {\n  tailwindcss: index,\n  'tailwindcss/index': index,\n  'tailwindcss/index.css': index,\n\n  'tailwindcss/preflight': preflight,\n  'tailwindcss/preflight.css': preflight,\n\n  'tailwindcss/theme': theme,\n  'tailwindcss/theme.css': theme,\n\n  'tailwindcss/utilities': utilities,\n  'tailwindcss/utilities.css': utilities,\n}\n"
  },
  {
    "path": "src/versions/v3.ts",
    "content": "// @ts-check\nimport * as path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport clearModule from 'clear-module'\nimport { createJiti } from 'jiti'\n// @ts-ignore\nimport { generateRules as generateRulesFallback } from 'tailwindcss-v3/lib/lib/generateRules'\n// @ts-ignore\nimport { createContext as createContextFallback } from 'tailwindcss-v3/lib/lib/setupContextUtils'\nimport resolveConfigFallback from 'tailwindcss-v3/resolveConfig'\nimport type { RequiredConfig } from 'tailwindcss-v3/types/config.js'\nimport type { UnifiedApi } from '../types'\nimport { bigSign } from '../utils'\n\ninterface LegacyTailwindContext {\n  tailwindConfig: {\n    prefix: string | ((selector: string) => string)\n  }\n\n  getClassOrder?: (classList: string[]) => [string, bigint | null][]\n\n  layerOrder: {\n    components: bigint\n  }\n}\n\ninterface GenerateRules {\n  (classes: Iterable<string>, context: LegacyTailwindContext): [bigint][]\n}\n\nfunction prefixCandidate(context: LegacyTailwindContext, selector: string): string {\n  let prefix = context.tailwindConfig.prefix\n  return typeof prefix === 'function' ? prefix(selector) : prefix + selector\n}\n\nexport async function loadV3(pkgDir: string | null, jsConfig: string | null): Promise<UnifiedApi> {\n  let createContext = createContextFallback\n  let generateRules: GenerateRules = generateRulesFallback\n  let resolveConfig = resolveConfigFallback\n  let tailwindConfig: RequiredConfig = { content: [] }\n\n  try {\n    if (pkgDir) {\n      resolveConfig = require(path.join(pkgDir, 'resolveConfig'))\n      createContext = require(path.join(pkgDir, 'lib/lib/setupContextUtils')).createContext\n      generateRules = require(path.join(pkgDir, 'lib/lib/generateRules')).generateRules\n    }\n  } catch {}\n\n  try {\n    if (jsConfig) {\n      clearModule(jsConfig)\n      let jiti = createJiti(import.meta.url, {\n        moduleCache: false,\n        fsCache: false,\n        interopDefault: true,\n      })\n      let url = pathToFileURL(jsConfig)\n      tailwindConfig = await jiti.import<RequiredConfig>(url.href, { default: true })\n    }\n  } catch (err) {\n    console.error(`Unable to load your Tailwind CSS v3 config: ${jsConfig}`)\n    throw err\n  }\n\n  // suppress \"empty content\" warning\n  tailwindConfig.content = ['no-op']\n\n  // Create the context\n  let context: LegacyTailwindContext = createContext(resolveConfig(tailwindConfig))\n\n  // Polyfill for older Tailwind CSS versions\n  function getClassOrderPolyfill(classes: string[]): [string, bigint | null][] {\n    // A list of utilities that are used by certain Tailwind CSS utilities but\n    // that don't exist on their own. This will result in them \"not existing\" and\n    // sorting could be weird since you still require them in order to make the\n    // host utitlies work properly. (Thanks Biology)\n    let parasiteUtilities = new Set([\n      prefixCandidate(context, 'group'),\n      prefixCandidate(context, 'peer'),\n    ])\n\n    let classNamesWithOrder: [string, bigint | null][] = []\n\n    for (let className of classes) {\n      let order: bigint | null =\n        generateRules(new Set([className]), context).sort(([a], [z]) => bigSign(z - a))[0]?.[0] ??\n        null\n\n      if (order === null && parasiteUtilities.has(className)) {\n        // This will make sure that it is at the very beginning of the\n        // `components` layer which technically means 'before any\n        // components'.\n        order = context.layerOrder.components\n      }\n\n      classNamesWithOrder.push([className, order])\n    }\n\n    return classNamesWithOrder\n  }\n\n  context.getClassOrder ??= getClassOrderPolyfill\n\n  return {\n    getClassOrder: (classList: string[]) => {\n      return context.getClassOrder\n        ? context.getClassOrder(classList)\n        : getClassOrderPolyfill(classList)\n    },\n  }\n}\n"
  },
  {
    "path": "src/versions/v4.ts",
    "content": "import * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { createJiti, type Jiti } from 'jiti'\nimport * as v4 from 'tailwindcss-v4'\nimport { resolveCssFrom, resolveJsFrom } from '../resolve'\nimport type { UnifiedApi } from '../types'\nimport { assets } from './assets'\n\ninterface DesignSystem {\n  getClassOrder(classList: string[]): [string, bigint | null][]\n}\n\ninterface LoadOptions {\n  base: string\n\n  loadModule?(\n    id: string,\n    base: string,\n    resourceType: string,\n  ): Promise<{\n    base: string\n    module: unknown\n  }>\n\n  loadPlugin?(id: string, base: string, resourceType: string): Promise<unknown>\n  loadConfig?(id: string, base: string, resourceType: string): Promise<unknown>\n\n  loadStylesheet?(\n    id: string,\n    base: string,\n  ): Promise<{\n    base: string\n    content: string\n  }>\n}\n\ninterface ApiV4 {\n  __unstable__loadDesignSystem(css: string, options: LoadOptions): Promise<DesignSystem>\n}\n\nexport async function loadV4(mod: ApiV4 | null, stylesheet: string | null): Promise<UnifiedApi> {\n  // This is not Tailwind v4\n  let isFallback = false\n  if (!mod || !mod.__unstable__loadDesignSystem) {\n    mod = v4 as ApiV4\n    isFallback = true\n  }\n\n  // Create a Jiti instance that can be used to load plugins and config files\n  let jiti = createJiti(import.meta.url, {\n    moduleCache: false,\n    fsCache: false,\n  })\n\n  let css: string\n  let importBasePath: string\n\n  if (stylesheet) {\n    // Resolve imports in the entrypoint to a flat CSS tree\n    css = await fs.readFile(stylesheet, 'utf-8')\n    importBasePath = path.dirname(stylesheet)\n  } else {\n    importBasePath = process.cwd()\n    stylesheet = path.join(importBasePath, 'fake.css')\n    css = assets['tailwindcss/theme.css']\n  }\n\n  // Load the design system and set up a compatible context object that is\n  // usable by the rest of the plugin\n  let design = await mod.__unstable__loadDesignSystem(css, {\n    base: importBasePath,\n\n    // v4.0.0-alpha.25+\n    loadModule: createLoader({\n      legacy: false,\n      jiti,\n      filepath: stylesheet,\n      onError: (id, err, resourceType) => {\n        console.error(`Unable to load ${resourceType}: ${id}`, err)\n\n        if (resourceType === 'config') {\n          return {}\n        } else if (resourceType === 'plugin') {\n          return () => {}\n        }\n      },\n    }),\n\n    loadStylesheet: async (id: string, base: string) => {\n      try {\n        let resolved = resolveCssFrom(base, id)\n\n        return {\n          base: path.dirname(resolved),\n          content: await fs.readFile(resolved, 'utf-8'),\n        }\n      } catch (err) {\n        if (isFallback && id in assets) {\n          return { base, content: assets[id] }\n        }\n\n        throw err\n      }\n    },\n\n    // v4.0.0-alpha.24 and below\n    loadPlugin: createLoader({\n      legacy: true,\n      jiti,\n      filepath: stylesheet,\n      onError(id, err) {\n        console.error(`Unable to load plugin: ${id}`, err)\n\n        return () => {}\n      },\n    }),\n\n    loadConfig: createLoader({\n      legacy: true,\n      jiti,\n      filepath: stylesheet,\n      onError(id, err) {\n        console.error(`Unable to load config: ${id}`, err)\n\n        return {}\n      },\n    }),\n  })\n\n  return {\n    getClassOrder: (classList: string[]) => {\n      return design.getClassOrder(classList)\n    },\n  }\n}\n\nfunction createLoader<T>({\n  legacy,\n  jiti,\n  filepath,\n  onError,\n}: {\n  legacy: true\n  jiti: Jiti\n  filepath: string\n  onError: (id: string, error: unknown, resourceType: string) => T\n}): (id: string) => Promise<unknown>\n\nfunction createLoader<T>({\n  legacy,\n  jiti,\n  filepath,\n  onError,\n}: {\n  legacy: false\n  jiti: Jiti\n  filepath: string\n  onError: (id: string, error: unknown, resourceType: string) => T\n}): (\n  id: string,\n  base: string,\n  resourceType: string,\n) => Promise<{\n  base: string\n  module: unknown\n}>\n\n/**\n * Create a loader function that can load plugins and config files relative to\n * the CSS file that uses them. However, we don't want missing files to prevent\n * everything from working so we'll let the error handler decide how to proceed.\n */\nfunction createLoader<T>({\n  legacy,\n  jiti,\n  filepath,\n  onError,\n}: {\n  legacy: boolean\n  jiti: Jiti\n  filepath: string\n  onError: (id: string, error: unknown, resourceType: string) => T\n}) {\n  let cacheKey = `${+Date.now()}`\n\n  async function loadFile(id: string, base: string, resourceType: string) {\n    try {\n      let resolved = resolveJsFrom(base, id)\n\n      let url = pathToFileURL(resolved)\n      url.searchParams.append('t', cacheKey)\n\n      return await jiti.import(url.href, { default: true })\n    } catch (err) {\n      return onError(id, err, resourceType)\n    }\n  }\n\n  if (legacy) {\n    let baseDir = path.dirname(filepath)\n    return (id: string) => loadFile(id, baseDir, 'module')\n  }\n\n  return async (id: string, base: string, resourceType: string) => {\n    return {\n      base,\n      module: await loadFile(id, base, resourceType),\n    }\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/basic/index.html",
    "content": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/basic/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/basic/prettier.config.js",
    "content": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/basic/tailwind.config.js",
    "content": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        tomato: 'tomato',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/cjs/index.html",
    "content": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/cjs/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/cjs/prettier.config.js",
    "content": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/cjs/tailwind.config.cjs",
    "content": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        hotpink: 'hotpink',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/custom-jsx/index.jsx",
    "content": "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\");\nconst d = sortMeTemplate`sm:p-1 p-2`;\nconst e = dontSortMeTemplate`sm:p-1 p-2`;\nconst f = tw.foo`sm:p-1 p-2`;\nconst g = tw.foo.bar`sm:p-1 p-2`;\nconst h = no.foo`sm:p-1 p-2`;\nconst i = no.tw`sm:p-1 p-2`;\nconst k = tw.foo('sm:p-1 p-2');\nconst l = tw.foo.bar('sm:p-1 p-2');\nconst m = no.foo('sm:p-1 p-2');\nconst n = no.tw('sm:p-1 p-2');\nconst o = tw(Foo)`sm:p-1 p-2`;\nconst p = tw(Foo)(Bar)`sm:p-1 p-2`;\nconst q = no(Foo)`sm:p-1 p-2`;\nconst r = no.tw(Foo)`sm:p-1 p-2`;\nconst s = tw(Foo)('sm:p-1 p-2');\nconst t = tw(Foo)(Bar)('sm:p-1 p-2');\nconst u = no(Foo)('sm:p-1 p-2');\nconst v = no.tw(Foo)('sm:p-1 p-2');\nconst w = tw.div(Foo)`sm:p-1 p-2`;\nconst x = tw(Foo).div`sm:p-1 p-2`;\nconst y = no.tw(Foo)`sm:p-1 p-2`;\nconst z = no(Foo).tw`sm:p-1 p-2`;\n\nconst A = (props) => <div className={props.sortMe} />;\nconst B = () => <A sortMe=\"sm:p-1 p-2\" dontSort=\"sm:p-1 p-2\" />;\n"
  },
  {
    "path": "tests/fixtures/custom-jsx/output.jsx",
    "content": "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\");\nconst d = sortMeTemplate`p-2 sm:p-1`;\nconst e = dontSortMeTemplate`sm:p-1 p-2`;\nconst f = tw.foo`p-2 sm:p-1`;\nconst g = tw.foo.bar`p-2 sm:p-1`;\nconst h = no.foo`sm:p-1 p-2`;\nconst i = no.tw`sm:p-1 p-2`;\nconst k = tw.foo(\"p-2 sm:p-1\");\nconst l = tw.foo.bar(\"p-2 sm:p-1\");\nconst m = no.foo(\"sm:p-1 p-2\");\nconst n = no.tw(\"sm:p-1 p-2\");\nconst o = tw(Foo)`p-2 sm:p-1`;\nconst p = tw(Foo)(Bar)`p-2 sm:p-1`;\nconst q = no(Foo)`sm:p-1 p-2`;\nconst r = no.tw(Foo)`sm:p-1 p-2`;\nconst s = tw(Foo)(\"p-2 sm:p-1\");\nconst t = tw(Foo)(Bar)(\"p-2 sm:p-1\");\nconst u = no(Foo)(\"sm:p-1 p-2\");\nconst v = no.tw(Foo)(\"sm:p-1 p-2\");\nconst w = tw.div(Foo)`p-2 sm:p-1`;\nconst x = tw(Foo).div`p-2 sm:p-1`;\nconst y = no.tw(Foo)`sm:p-1 p-2`;\nconst z = no(Foo).tw`sm:p-1 p-2`;\n\nconst A = (props) => <div className={props.sortMe} />;\nconst B = () => <A sortMe=\"p-2 sm:p-1\" dontSort=\"sm:p-1 p-2\" />;\n"
  },
  {
    "path": "tests/fixtures/custom-jsx/prettier.config.js",
    "content": "module.exports = {\n  tailwindFunctions: ['sortMeFn', 'sortMeTemplate', 'tw'],\n  tailwindAttributes: ['sortMe'],\n};\n"
  },
  {
    "path": "tests/fixtures/custom-jsx/tailwind.config.js",
    "content": "module.exports = {\n  theme: {},\n}\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v3/config.js",
    "content": "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",
    "content": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v3/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v3/package.json",
    "content": "{\n  \"dependencies\": {\n    \"tailwindcss-v3\": \"npm:tailwindcss@^3.4.17\"\n  },\n  \"prettier\": {\n    \"tailwindPackageName\": \"tailwindcss-v3\",\n    \"tailwindConfig\": \"./config.js\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v4/app.css",
    "content": "@import 'tailwindcss-v4';\n\n@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v4/index.html",
    "content": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v4/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/custom-pkg-name-v4/package.json",
    "content": "{\n  \"dependencies\": {\n    \"tailwindcss-v4\": \"npm:tailwindcss@^4.1.7\"\n  },\n  \"prettier\": {\n    \"tailwindPackageName\": \"tailwindcss-v4\",\n    \"tailwindStylesheet\": \"./app.css\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/custom-vue/index.vue",
    "content": "<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 p-2\");\n  let d = sortMeTemplate`sm:p-1 p-2`;\n  let e = dontSortMeTemplate`sm:p-1 p-2`;\n</script>\n<template>\n  <div class=\"sm:p-1 p-2\" sortMe=\"sm:p-1 p-2\" dontSortMe=\"sm:p-1 p-2\"></div>\n  <div :class=\"{'sm:p-1 p-2': true}\"></div>\n  <div :sortMe=\"{'sm:p-1 p-2': true}\"></div>\n</template>\n"
  },
  {
    "path": "tests/fixtures/custom-vue/output.vue",
    "content": "<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\");\nlet d = sortMeTemplate`p-2 sm:p-1`;\nlet e = dontSortMeTemplate`sm:p-1 p-2`;\n</script>\n<template>\n  <div class=\"p-2 sm:p-1\" sortMe=\"p-2 sm:p-1\" dontSortMe=\"sm:p-1 p-2\"></div>\n  <div :class=\"{ 'p-2 sm:p-1': true }\"></div>\n  <div :sortMe=\"{ 'p-2 sm:p-1': true }\"></div>\n</template>\n"
  },
  {
    "path": "tests/fixtures/custom-vue/prettier.config.js",
    "content": "module.exports = {\n  tailwindFunctions: ['sortMeFn', 'sortMeTemplate'],\n  tailwindAttributes: ['sortMe'],\n};\n"
  },
  {
    "path": "tests/fixtures/custom-vue/tailwind.config.js",
    "content": "module.exports = {\n  theme: {},\n}\n"
  },
  {
    "path": "tests/fixtures/esm/index.html",
    "content": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/esm/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/esm/prettier.config.js",
    "content": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/esm/tailwind.config.mjs",
    "content": "export default {\n  theme: {\n    extend: {\n      colors: {\n        hotpink: \"hotpink\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "tests/fixtures/esm-explicit/config.mjs",
    "content": "export default {\n  theme: {\n    extend: {\n      colors: {\n        hotpink: \"hotpink\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "tests/fixtures/esm-explicit/index.html",
    "content": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/esm-explicit/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/esm-explicit/prettier.config.js",
    "content": "module.exports = {\n  tailwindConfig: './config.mjs'\n};\n"
  },
  {
    "path": "tests/fixtures/monorepo/.prettierrc",
    "content": "{\n  \"tailwindFunctions\": [\"tw\"],\n  \"overrides\": [\n    {\n      \"files\": \"package-1/**\",\n      \"options\": {\n        \"tailwindStylesheet\": \"./package-1/app.css\"\n      }\n    },\n    {\n      \"files\": \"package-2/**\",\n      \"options\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-1/app.css",
    "content": "@import 'tailwindcss';\n\n@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-1/index.jsx",
    "content": "const a = tw`sm:bg-tomato bg-red-500`;\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-1/output.jsx",
    "content": "const a = tw`bg-red-500 sm:bg-tomato`;\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-1/package.json",
    "content": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-2/index.jsx",
    "content": "const a = tw`sm:bg-tomato bg-red-500`;\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-2/output.jsx",
    "content": "const a = tw`bg-red-500 sm:bg-tomato`;\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-2/package.json",
    "content": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"^3.4.17\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/monorepo/package-2/tailwind.config.js",
    "content": "module.exports = { theme: { extend: { colors: { tomato: 'tomato' } } } }\n"
  },
  {
    "path": "tests/fixtures/monorepo/package.json",
    "content": "{}\n"
  },
  {
    "path": "tests/fixtures/no-local-version/app.css",
    "content": "@import \"tailwindcss\";\n\n@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures/no-local-version/index.html",
    "content": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-local-version/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-local-version/package.json",
    "content": "{\n  \"prettier\": {\n    \"tailwindStylesheet\": \"./app.css\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/no-prettier-config/index.html",
    "content": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-prettier-config/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-prettier-config/tailwind.config.js",
    "content": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        tomato: 'tomato',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/no-stylesheet-given/index.html",
    "content": "<div class=\"sm:bg-red-500 bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/no-stylesheet-given/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/package.json",
    "content": "{\n  \"type\": \"commonjs\"\n}\n"
  },
  {
    "path": "tests/fixtures/plugins/index.html",
    "content": "<div class=\"sm:bar foo uppercase\"></div>\n"
  },
  {
    "path": "tests/fixtures/plugins/output.html",
    "content": "<div class=\"uppercase foo sm:bar\"></div>\n"
  },
  {
    "path": "tests/fixtures/plugins/prettier.config.js",
    "content": "const prettier = require(\"prettier\")\n\nmodule.exports = {\n  plugins: ['../../../dist/index.mjs'],\n}\n"
  },
  {
    "path": "tests/fixtures/plugins/tailwind.config.js",
    "content": "const plugin = require(\"tailwindcss-v3/plugin\");\n\nmodule.exports = {\n  plugins: [\n    plugin(function ({ addUtilities }) {\n      addUtilities({\n        \".foo\": { color: \"red\" },\n        \".bar\": { color: \"blue\" },\n      });\n    }),\n  ],\n};\n"
  },
  {
    "path": "tests/fixtures/ts/index.html",
    "content": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/ts/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/ts/prettier.config.js",
    "content": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/ts/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n  content: [\"index.html\"],\n  theme: {\n    extend: {\n      colors: {\n        hotpink: \"hotpink\",\n      },\n    },\n  },\n} satisfies Config;\n"
  },
  {
    "path": "tests/fixtures/ts-explicit/config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n  content: [\"index.html\"],\n  theme: {\n    extend: {\n      colors: {\n        hotpink: \"hotpink\",\n      },\n    },\n  },\n} satisfies Config;\n"
  },
  {
    "path": "tests/fixtures/ts-explicit/index.html",
    "content": "<div class=\"sm:bg-hotpink bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/ts-explicit/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-hotpink\"></div>\n"
  },
  {
    "path": "tests/fixtures/ts-explicit/prettier.config.js",
    "content": "module.exports = {\n  tailwindConfig: './config.ts'\n};\n"
  },
  {
    "path": "tests/fixtures/v3-2/index.html",
    "content": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/v3-2/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/v3-2/package.json",
    "content": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"3.2.7\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/v3-2/prettier.config.js",
    "content": "module.exports = {};\n"
  },
  {
    "path": "tests/fixtures/v3-2/tailwind.config.js",
    "content": "module.exports = {\n  theme: {\n    extend: {\n      colors: {\n        tomato: 'tomato',\n      },\n    },\n  },\n}\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/index.html",
    "content": "<div class=\"sm:p-0 p-0\"></div>\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/output.html",
    "content": "<div class=\"p-0 sm:p-0\"></div>\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/package.json",
    "content": "{\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",
    "content": "import { createJiti } from 'jiti'\n\nconst jiti = createJiti(import.meta.url, {\n  interopDefault: true,\n})\n\nconst _module = await jiti.import(new URL('../src/index.ts', import.meta.url).href, {\n  default: true,\n})\n\nexport default _module?.default ?? _module\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/packages/tailwind-config/package.json",
    "content": "{\n  \"name\": \"@repo/tailwind-config\",\n  \"version\": \"1.0.0\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.mjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"exports\": {\n    \".\": {\n      \"import\": \"./dist/index.mjs\",\n      \"require\": \"./dist/index.mjs\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"dependencies\": {\n    \"jiti\": \"^2.6.1\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/packages/tailwind-config/src/index.ts",
    "content": "export default {\n  content: ['./index.html'],\n}\n"
  },
  {
    "path": "tests/fixtures/v3-jiti-reexport/tailwind.config.mjs",
    "content": "export { default } from '@repo/tailwind-config'\n"
  },
  {
    "path": "tests/fixtures/v4/basic/app.css",
    "content": "@import 'tailwindcss';\n\n@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures/v4/basic/index.html",
    "content": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/v4/basic/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/v4/basic/package.json",
    "content": "{\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",
    "content": "@import 'tailwindcss';\n\n/* Load ESM versions */\n@config './esm/my-config.mjs';\n@plugin './esm/my-plugin.mjs';\n\n/* Load Common JS versions */\n@config './cjs/my-config.cjs';\n@plugin './cjs/my-plugin.cjs';\n\n/* Load TypeScript versions */\n@config './ts/my-config.ts';\n@plugin './ts/my-plugin.ts';\n\n/* Attempt to load files that do not exist */\n@config './missing-confg.mjs';\n@plugin './missing-plugin.mjs';\n\n@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/cjs/my-config.cjs",
    "content": "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",
    "content": "const plugin = require('tailwindcss/plugin')\n\nmodule.exports = plugin(\n  ({ addUtilities }) => {\n    addUtilities({\n      '.utility-cjs-from-plugin': {\n        color: 'black'\n      },\n      '.utility-cjs-from-plugin-2': {\n        width: '100%',\n        height: '100%',\n      },\n    })\n  },\n  {\n    theme: {\n      extend: {\n        colors: {\n          'cjs-from-plugin': 'black',\n        },\n      },\n    },\n  },\n)\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/esm/my-config.mjs",
    "content": "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",
    "content": "import plugin from 'tailwindcss/plugin'\n\nexport default plugin(\n  ({ addUtilities }) => {\n    addUtilities({\n      '.utility-esm-from-plugin': {\n        color: 'black'\n      },\n      '.utility-esm-from-plugin-2': {\n        width: '100%',\n        height: '100%',\n      },\n    })\n  },\n  {\n    theme: {\n      extend: {\n        colors: {\n          'esm-from-plugin': 'black',\n        },\n      },\n    },\n  },\n)\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/index.html",
    "content": "<div\n  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\"\n></div>\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/output.html",
    "content": "<div\n  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\"\n></div>\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/package.json",
    "content": "{\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",
    "content": "import type { Config } from 'tailwindcss'\n\nexport default {\n  theme: {\n    extend: {\n      colors: {\n        'ts-from-config': 'black',\n      },\n    },\n  },\n} satisfies Config\n"
  },
  {
    "path": "tests/fixtures/v4/css-loading-js/ts/my-plugin.ts",
    "content": "import plugin from 'tailwindcss/plugin'\n\nexport default plugin(\n  ({ addUtilities }) => {\n    addUtilities({\n      '.utility-ts-from-plugin': {\n        color: 'black'\n      },\n      '.utility-ts-from-plugin-2': {\n        width: '100%',\n        height: '100%',\n      },\n    })\n  },\n  {\n    theme: {\n      extend: {\n        colors: {\n          'ts-from-plugin': 'black',\n        },\n      },\n    },\n  },\n)\n"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/app.css",
    "content": "@import 'tailwindcss';\n@import '#theme';\n"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/index.html",
    "content": "<div class=\"sm:bg-tomato bg-red-500\"></div>\n"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/output.html",
    "content": "<div class=\"bg-red-500 sm:bg-tomato\"></div>\n"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/package.json",
    "content": "{\n  \"dependencies\": {\n    \"tailwindcss\": \"^4.0.0\"\n  },\n  \"prettier\": {\n    \"tailwindStylesheet\": \"./app.css\"\n  },\n  \"imports\": {\n    \"#theme\": \"./theme.css\"\n  }\n}\n"
  },
  {
    "path": "tests/fixtures/v4/subpath-imports/theme.css",
    "content": "@theme {\n  --color-tomato: tomato;\n}\n"
  },
  {
    "path": "tests/fixtures.test.ts",
    "content": "import { exec } from 'node:child_process'\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { promisify } from 'node:util'\nimport { afterAll, beforeAll, describe, test } from 'vitest'\nimport { format, pluginPath } from './utils'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\nconst execAsync = promisify(exec)\n\nlet fixtures = [\n  {\n    name: 'no prettier config',\n    dir: 'no-prettier-config',\n    ext: 'html',\n  },\n  {\n    name: 'no local install of Tailwind CSS (uses v4)',\n    dir: 'no-local-version',\n    ext: 'html',\n  },\n  {\n    name: 'no stylesheet given (uses v4)',\n    dir: 'no-stylesheet-given',\n    ext: 'html',\n  },\n  {\n    name: 'inferred config path',\n    dir: 'basic',\n    ext: 'html',\n  },\n  {\n    name: 'inferred config path (.cjs)',\n    dir: 'cjs',\n    ext: 'html',\n  },\n  {\n    name: 'using esm config',\n    dir: 'esm',\n    ext: 'html',\n  },\n  {\n    name: 'using esm config (explicit path)',\n    dir: 'esm-explicit',\n    ext: 'html',\n  },\n  {\n    name: 'using ts config',\n    dir: 'ts',\n    ext: 'html',\n  },\n  {\n    name: 'using ts config (explicit path)',\n    dir: 'ts-explicit',\n    ext: 'html',\n  },\n  {\n    name: 'using v3.2.7',\n    dir: 'v3-2',\n    ext: 'html',\n  },\n  {\n    name: 'v3: re-exported config with jiti',\n    dir: 'v3-jiti-reexport',\n    ext: 'html',\n  },\n  {\n    name: 'plugins',\n    dir: 'plugins',\n    ext: 'html',\n  },\n  {\n    name: 'customizations: js/jsx',\n    dir: 'custom-jsx',\n    ext: 'jsx',\n  },\n\n  {\n    name: 'v4: basic formatting',\n    dir: 'v4/basic',\n    ext: 'html',\n  },\n  {\n    name: 'v4: configs and plugins',\n    dir: 'v4/css-loading-js',\n    ext: 'html',\n  },\n  {\n    name: 'v4: subpath imports',\n    dir: 'v4/subpath-imports',\n    ext: 'html',\n  },\n  {\n    name: 'custom npm package name: v3',\n    dir: 'custom-pkg-name-v3',\n    ext: 'html',\n  },\n  {\n    name: 'custom npm package name: v4',\n    dir: 'custom-pkg-name-v4',\n    ext: 'html',\n  },\n\n  {\n    name: 'monorepo / v4',\n    dir: 'monorepo/package-1',\n    ext: 'jsx',\n  },\n  {\n    name: 'monorepo / v3',\n    dir: 'monorepo/package-2',\n    ext: 'jsx',\n  },\n]\n\nlet configs = [\n  {\n    from: __dirname + '/../.prettierignore',\n    to: __dirname + '/../.prettierignore.testing',\n  },\n  {\n    from: __dirname + '/../prettier.config.js',\n    to: __dirname + '/../prettier.config.js.testing',\n  },\n]\n\ntest.concurrent('explicit config path', async ({ expect }) => {\n  expect(\n    await format('<div class=\"sm:bg-tomato bg-red-500\"></div>', {\n      tailwindConfig: path.resolve(__dirname, 'fixtures/basic/tailwind.config.js'),\n    }),\n  ).toEqual('<div class=\"bg-red-500 sm:bg-tomato\"></div>')\n})\n\ndescribe('fixtures', () => {\n  // Temporarily move config files out of the way so they don't interfere with the tests\n  beforeAll(async () => {\n    await Promise.all(configs.map(({ from, to }) => fs.rename(from, to)))\n  })\n\n  afterAll(async () => {\n    await Promise.all(configs.map(({ from, to }) => fs.rename(to, from)))\n  })\n\n  let binPath = path.resolve(__dirname, '../node_modules/.bin/prettier')\n\n  for (const { ext, dir, name } of fixtures) {\n    let fixturePath = path.resolve(__dirname, `fixtures/${dir}`)\n    let inputPath = path.resolve(fixturePath, `index.${ext}`)\n    let outputPath = path.resolve(fixturePath, `output.${ext}`)\n    let cmd = `${binPath} ${inputPath} --plugin ${pluginPath}`\n\n    test.concurrent(name, async ({ expect }) => {\n      let results = await execAsync(cmd)\n      let formatted = results.stdout.replace(/\\r\\n/g, '\\n')\n      let expected = await fs.readFile(outputPath, 'utf-8').then((c) => c.replace(/\\r\\n/g, '\\n'))\n\n      expect(formatted.trim()).toEqual(expected.trim())\n    })\n  }\n})\n"
  },
  {
    "path": "tests/format.test.ts",
    "content": "import { describe, test } from 'vitest'\nimport { tests } from './tests.js'\nimport { format } from './utils.js'\n\ndescribe('parsers', async () => {\n  for (let parser in tests) {\n    test(parser, async ({ expect }) => {\n      for (let [input, expected, options] of tests[parser]) {\n        expect(await format(input, { ...options, parser })).toEqual(expected)\n      }\n    })\n  }\n})\n\ndescribe('other', () => {\n  test('non-tailwind classes are sorted to the front', async ({ expect }) => {\n    let result = await format('<div class=\"sm:lowercase uppercase potato text-sm\"></div>')\n\n    expect(result).toEqual('<div class=\"potato text-sm uppercase sm:lowercase\"></div>')\n  })\n\n  test('parasite utilities (v3)', async ({ expect }) => {\n    let result = await format('<div class=\"group peer unknown-class p-0 container\"></div>', {\n      tailwindPackageName: 'tailwindcss-v3',\n    })\n\n    expect(result).toEqual('<div class=\"unknown-class group peer container p-0\"></div>')\n  })\n\n  test('parasite utilities (v4)', async ({ expect }) => {\n    let result = await format('<div class=\"group peer unknown-class p-0 container\"></div>', {\n      tailwindPackageName: 'tailwindcss-v4',\n    })\n\n    expect(result).toEqual('<div class=\"group peer unknown-class container p-0\"></div>')\n  })\n\n  test('parasite utilities (no install == v4)', async ({ expect }) => {\n    let result = await format('<div class=\"group peer unknown-class p-0 container\"></div>')\n\n    expect(result).toEqual('<div class=\"group peer unknown-class container p-0\"></div>')\n  })\n})\n\ndescribe('whitespace', () => {\n  test('class lists containing interpolation are ignored', async ({ expect }) => {\n    let result = await format('<div class=\"{{ this is ignored }}\"></div>')\n\n    expect(result).toEqual('<div class=\"{{ this is ignored }}\"></div>')\n  })\n\n  test('whitespace can be preserved around classes', async ({ expect }) => {\n    let result = await format(`;<div className={' underline text-red-500  flex '}></div>`, {\n      parser: 'babel',\n      tailwindPreserveWhitespace: true,\n    })\n\n    expect(result).toEqual(`;<div className={' flex text-red-500  underline '}></div>`)\n  })\n\n  test('whitespace can be collapsed around classes', async ({ expect }) => {\n    let result = await format('<div class=\" underline text-red-500  flex \"></div>')\n\n    expect(result).toEqual('<div class=\"flex text-red-500 underline\"></div>')\n  })\n\n  test('whitespace is collapsed but not trimmed when ignored', async ({ expect }) => {\n    let result = await format(';<div className={`underline text-red-500 ${foo}-bar flex`}></div>', {\n      parser: 'babel',\n    })\n\n    expect(result).toEqual(';<div className={`text-red-500 underline ${foo}-bar flex`}></div>')\n  })\n\n  test('whitespace is not trimmed inside concat expressions', async ({ expect }) => {\n    let result = await format(\";<div className={a + ' p-4 ' + b}></div>\", {\n      parser: 'babel',\n    })\n\n    expect(result).toEqual(\";<div className={a + ' p-4 ' + b}></div>\")\n  })\n\n  test('whitespace is not trimmed inside concat expressions (angular)', async ({ expect }) => {\n    let result = await format(\n      `<ul [class]=\"'pagination' + (size ? ' pagination-' + size : '')\"></ul>`,\n      {\n        parser: 'angular',\n      },\n    )\n\n    expect(result).toEqual(`<ul [class]=\"'pagination' + (size ? ' pagination-' + size : '')\"></ul>`)\n  })\n\n  test('whitespace is not trimmed inside adjacent-before/after template expressions', async ({\n    expect,\n  }) => {\n    let result = await format(\n      \";<div className={`header${isExtendable ? ' header-extendable' : ''}`} />\",\n      {\n        parser: 'babel',\n      },\n    )\n\n    expect(result).toEqual(\n      \";<div className={`header${isExtendable ? ' header-extendable' : ''}`} />\",\n    )\n  })\n\n  test('whitespace is not trimmed before template literal quasis without leading space', async ({\n    expect,\n  }) => {\n    let result = await format(\";<div className={`${foo ? 'sm:p-0 p-0 ' : ''}header`}></div>\", {\n      parser: 'babel',\n    })\n\n    expect(result).toEqual(\";<div className={`${foo ? 'p-0 sm:p-0 ' : ''}header`}></div>\")\n  })\n\n  test('duplicate classes are dropped', async ({ expect }) => {\n    let result = await format('<div class=\"underline line-through underline flex\"></div>')\n\n    expect(result).toEqual('<div class=\"flex line-through underline\"></div>')\n  })\n})\n\ndescribe('errors', () => {\n  test('when the given JS config does not exist', async ({ expect }) => {\n    let result = format('<div></div>', {\n      tailwindConfig: 'i-do-not-exist.js',\n      tailwindPackageName: 'tailwindcss-v3',\n    })\n\n    await expect(result).rejects.toThrowError(/Cannot find module/)\n  })\n\n  test('when the given stylesheet does not exist', async ({ expect }) => {\n    let result = format('<div></div>', {\n      tailwindStylesheet: 'i-do-not-exist.css',\n      tailwindPackageName: 'tailwindcss-v4',\n    })\n\n    await expect(result).rejects.toThrowError(/no such file or directory/)\n  })\n\n  test('when using a stylesheet and the local install is not v4', async ({ expect }) => {\n    let result = format('<div></div>', {\n      tailwindStylesheet: 'i-do-not-exist.css',\n      tailwindPackageName: 'tailwindcss-v3',\n    })\n\n    await expect(result).rejects.toThrowError(/no such file or directory/)\n  })\n})\n\ndescribe('regex matching', () => {\n  test('attribute name exact matches', async ({ expect }) => {\n    let result = await format('<div myClass=\"sm:p-0 p-0\"></div>', {\n      tailwindAttributes: ['myClass'],\n    })\n\n    expect(result).toEqual('<div myClass=\"p-0 sm:p-0\"></div>')\n  })\n\n  test('function name exact matches', async ({ expect }) => {\n    let result = await format('let classList = tw`sm:p-0 p-0`', {\n      parser: 'babel',\n      tailwindFunctions: ['tw'],\n    })\n\n    expect(result).toEqual('let classList = tw`p-0 sm:p-0`')\n  })\n\n  test('attribute name regex matches', async ({ expect }) => {\n    let result = await format(\n      `<div data-class=\"sm:p-0 p-0\" data-classes=\"sm:p-0 p-0\" data-style=\"sm:p-0 p-0\"></div>`,\n      {\n        tailwindAttributes: ['/data-.*/'],\n      },\n    )\n\n    expect(result).toEqual(\n      `<div data-class=\"p-0 sm:p-0\" data-classes=\"p-0 sm:p-0\" data-style=\"p-0 sm:p-0\"></div>`,\n    )\n  })\n\n  test('function name regex matches', async ({ expect }) => {\n    let result = await format(\n      'let classList1 = twClasses`sm:p-0 p-0`\\nlet classList2 = myClasses`sm:p-0 p-0`',\n      {\n        parser: 'babel',\n        tailwindFunctions: ['/.*Classes/'],\n      },\n    )\n\n    expect(result).toEqual(\n      'let classList1 = twClasses`p-0 sm:p-0`\\nlet classList2 = myClasses`p-0 sm:p-0`',\n    )\n  })\n\n  test('regex flags are supported', async ({ expect }) => {\n    let result = await format(`;<div MyClass=\"sm:p-0 p-0\" data-other={MyFn('sm:p-0 p-0')} />`, {\n      parser: 'babel',\n      tailwindAttributes: ['/myclass/i'],\n      tailwindFunctions: ['/myfn/i'],\n    })\n\n    expect(result).toEqual(`;<div MyClass=\"p-0 sm:p-0\" data-other={MyFn('p-0 sm:p-0')} />`)\n  })\n\n  test('anchors are supported', async ({ expect }) => {\n    let result = await format(\n      `;<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')} />`,\n      {\n        parser: 'babel',\n        tailwindAttributes: ['/.*List$/'],\n        tailwindFunctions: ['/.*List$/'],\n      },\n    )\n\n    expect(result).toEqual(\n      `;<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')} />`,\n    )\n  })\n\n  test('works with Vue dynamic bindings', async ({ expect }) => {\n    let result = await format('<div :data-classes=\"`sm:p-0 p-0`\"></div>', {\n      parser: 'vue',\n      tailwindAttributes: ['/data-.*/'],\n    })\n\n    expect(result).toEqual('<div :data-classes=\"`p-0 sm:p-0`\"></div>')\n  })\n\n  test('works with Angular property bindings', async ({ expect }) => {\n    let result = await format('<div [dataClasses]=\"`sm:p-0 p-0`\"></div>', {\n      parser: 'angular',\n      tailwindAttributes: ['/data.*/i'],\n    })\n\n    expect(result).toEqual('<div [dataClasses]=\"`p-0 sm:p-0`\"></div>')\n  })\n\n  test('invalid regex patterns do nothing', async ({ expect }) => {\n    let result = await format('<div data-test=\"sm:p-0 p-0\"></div>', {\n      tailwindAttributes: ['/data-[/'],\n    })\n\n    expect(result).toEqual('<div data-test=\"sm:p-0 p-0\"></div>')\n  })\n\n  test('dynamic attributes are not matched as static attributes', async ({ expect }) => {\n    let result = await format(`<div :custom-class=\"['sm:p-0 flex underline p-0']\"></div>`, {\n      parser: 'vue',\n      tailwindAttributes: ['/.*-class/'],\n    })\n\n    expect(result).toEqual(`<div :custom-class=\"['flex p-0 underline sm:p-0']\"></div>`)\n  })\n\n  test('dynamic attributes are not matched as static attributes (2)', async ({ expect }) => {\n    let result = await format(`<div :custom-class=\"['sm:p-0 flex underline p-0']\"></div>`, {\n      parser: 'vue',\n      tailwindAttributes: ['/:custom-class/'],\n    })\n\n    expect(result).toEqual(`<div :custom-class=\"['sm:p-0 flex underline p-0']\"></div>`)\n  })\n\n  // These tests pass but that is a side-effect of the implementation\n  // If these change in the future to no longer pass that is a good thing\n  describe('dynamic attribute matching quirks', () => {\n    test('Vue', async ({ expect }) => {\n      let result = await format('<div ::data-classes=\"`sm:p-0 p-0`\"></div>', {\n        parser: 'vue',\n        tailwindAttributes: ['/:data-.*/'],\n      })\n\n      expect(result).toEqual('<div ::data-classes=\"`p-0 sm:p-0`\"></div>')\n    })\n\n    test('Angular', async ({ expect }) => {\n      let result = await format('<div [[dataClasses]]=\"`sm:p-0 p-0`\"></div>', {\n        parser: 'angular',\n        tailwindAttributes: ['/\\\\[data.*\\\\]/i'],\n      })\n\n      expect(result).toEqual('<div [[dataClasses]]=\"`p-0 sm:p-0`\"></div>')\n    })\n  })\n})\n"
  },
  {
    "path": "tests/plugins.test.ts",
    "content": "import { createRequire } from 'node:module'\nimport dedent from 'dedent'\nimport { test } from 'vitest'\nimport { javascript } from './tests.js'\nimport type { TestEntry } from './utils.js'\nimport { format, no, pluginPath, t, yes } from './utils.js'\n\nconst require = createRequire(import.meta.url)\n\ninterface PluginTest {\n  plugins: string[]\n  options?: Record<string, any>\n  tests: Record<string, TestEntry[]>\n}\n\nlet tests: PluginTest[] = [\n  {\n    plugins: ['@trivago/prettier-plugin-sort-imports'],\n    options: {\n      importOrder: ['^@one/(.*)$', '^@two/(.*)$', '^[./]'],\n      importOrderSortSpecifiers: true,\n    },\n    tests: {\n      babel: [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import '@one/file'\\nimport '@two/file'\\nimport './three'`,\n        ],\n      ],\n      typescript: [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import '@one/file'\\nimport '@two/file'\\nimport './three'`,\n        ],\n      ],\n\n      // This plugin does not support babel-ts\n      'babel-ts': [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['@ianvs/prettier-plugin-sort-imports'],\n    options: {\n      importOrder: ['^@tailwindcss/(.*)$', '^@babel/(.*)$', '^[./]'],\n      importOrderSortSpecifiers: true,\n    },\n    tests: {\n      babel: [\n        [\n          `import './i-haz-side-effects'\\nimport i3 from './three'\\nimport i2 from '@two/file'\\nimport i1 from '@one/file'`,\n          `import './i-haz-side-effects'\\nimport i1 from '@one/file'\\nimport i2 from '@two/file'\\nimport i3 from './three'`,\n        ],\n      ],\n      typescript: [\n        [\n          `import './i-haz-side-effects'\\nimport i3 from './three'\\nimport i2 from '@two/file'\\nimport i1 from '@one/file'`,\n          `import './i-haz-side-effects'\\nimport i1 from '@one/file'\\nimport i2 from '@two/file'\\nimport i3 from './three'`,\n        ],\n      ],\n\n      // This plugin does not support babel-ts\n      'babel-ts': [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['prettier-plugin-sort-imports'],\n    options: {\n      sortingMethod: 'alphabetical',\n    },\n    tests: {\n      babel: [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import './three'\\nimport '@one/file'\\nimport '@two/file'`,\n        ],\n      ],\n      typescript: [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import './three'\\nimport '@one/file'\\nimport '@two/file'`,\n        ],\n      ],\n\n      // This plugin does not support babel-ts\n      'babel-ts': [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['prettier-plugin-multiline-arrays'],\n    tests: {\n      babel: [[`const array = [\\n'one']`, `const array = [\\n  'one',\\n]`]],\n      typescript: [[`const array = [\\n'one']`, `const array = [\\n  'one',\\n]`]],\n      'babel-ts': [[`const array = [\\n'one']`, `const array = [\\n  'one',\\n]`]],\n    },\n  },\n  {\n    plugins: ['prettier-plugin-organize-imports'],\n    options: {},\n    tests: {\n      babel: [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import '@one/file'\\nimport '@two/file'\\nimport './three'`,\n        ],\n      ],\n      typescript: [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import '@one/file'\\nimport '@two/file'\\nimport './three'`,\n        ],\n      ],\n      'babel-ts': [\n        [\n          `import './three'\\nimport '@two/file'\\nimport '@one/file'`,\n          `import '@one/file'\\nimport '@two/file'\\nimport './three'`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['@zackad/prettier-plugin-twig'],\n    options: {\n      twigAlwaysBreakObjects: false,\n      tailwindFunctions: ['addClass', 'tw'],\n    },\n    tests: {\n      twig: [\n        [\n          `<section class=\"{{ {base:css.prices}|classes }}\"></section>`,\n          `<section class=\"{{ { base: css.prices }|classes }}\"></section>`,\n        ],\n        t`<section class=\"${yes}\"></section>`,\n\n        t`<section class=\"${yes} text-{{ i }}\"></section>`,\n        t`<section class=\"${yes} {{ i }}-text\"></section>`,\n        t`<section class=\"text-{{ i }} ${yes}\"></section>`,\n        t`<section class=\"{{ i }}-text ${yes}\"></section>`,\n\n        // text-center is used because it's placed between p-0 and sm:p-0\n        t`<section class=\"${yes} text-center{{ i }}\"></section>`,\n        t`<section class=\"${yes} {{ i }}text-center\"></section>`,\n        t`<section class=\"text-center{{ i }} ${yes}\"></section>`,\n        t`<section class=\"{{ i }}text-center ${yes}\"></section>`,\n\n        [\n          `<div class=\" sm:flex   underline  block\"></div>`,\n          `<div class=\"block underline sm:flex\"></div>`,\n        ],\n        [\n          `<div class=\"{{ ' flex ' + ' underline ' + ' block ' }}\"></div>`,\n          `<div class=\"{{ 'flex ' + ' underline' + ' block' }}\"></div>`,\n        ],\n\n        // Function call tests\n        t`<div {{ tw('${yes}') }}></div>`,\n        t`<div {{ attributes.addClass('${yes}') }}></div>`,\n\n        t`{{ tw('${yes}') }}`,\n        t`{{ attributes.addClass('${yes}') }}`,\n\n        t`{{ tw('${yes}').tw('${yes}').tw('${yes}') }}`,\n        t`{{ attributes.addClass('${yes}').addClass('${yes}').addClass('${yes}') }}`,\n\n        t`{% set className = '${no}' %} {{ attributes.addClass(className) }}`,\n        [\n          `{{ attributes.addClass(\"sm:p-0 \" ~ variant ~ \" p-0\") }}`,\n          `{{ attributes.addClass('sm:p-0 ' ~ variant ~ ' p-0') }}`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['@prettier/plugin-hermes'],\n    tests: {\n      hermes: javascript,\n    },\n  },\n  {\n    plugins: ['@prettier/plugin-oxc'],\n    tests: {\n      oxc: javascript,\n      'oxc-ts': javascript,\n    },\n  },\n  {\n    plugins: ['@prettier/plugin-pug'],\n    tests: {\n      pug: [\n        [\n          `a(class='md:p-4 sm:p-0 p-4 bg-blue-600' href='//example.com') Example`,\n          `a.bg-blue-600.p-4(class='sm:p-0 md:p-4', href='//example.com') Example`,\n        ],\n        [\n          `a.p-4.bg-blue-600(class='sm:p-0 md:p-4', href='//example.com') Example`,\n          `a.bg-blue-600.p-4(class='sm:p-0 md:p-4', href='//example.com') Example`,\n        ],\n\n        [\n          `a.p-4.bg-blue-600(class=' sm:p-0     md:p-4 ', href='//example.com') Example`,\n          `a.bg-blue-600.p-4(class='sm:p-0 md:p-4', href='//example.com') Example`,\n        ],\n\n        // These two tests show how our sorting the two class lists separately is suboptimal\n        // Two consecutive saves will result in different output\n        // Where the second save is the most correct\n        [\n          `a.p-4(class='bg-blue-600 sm:p-0 md:p-4', href='//example.com') Example`,\n          `a.p-4.bg-blue-600(class='sm:p-0 md:p-4', href='//example.com') Example`,\n        ],\n        [\n          `a.p-4.bg-blue-600(class='sm:p-0 md:p-4', href='//example.com') Example`,\n          `a.bg-blue-600.p-4(class='sm:p-0 md:p-4', href='//example.com') Example`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['prettier-plugin-jsdoc'],\n    tests: {\n      babel: [\n        [\n          `/**\\n             * @param {  string   }    param0 description\\n             */\\n            export default function Foo(param0) { return <div className=\"sm:p-0 p-4\"></div> }`,\n          `/** @param {string} param0 Description */\\nexport default function Foo(param0) {\\n  return <div className=\"p-4 sm:p-0\"></div>\\n}`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['prettier-plugin-css-order'],\n    tests: {\n      css: [\n        [\n          `.foo {\\n  color: red;\\n  background-color: blue;\\n  @apply sm:p-0 p-4 bg-blue-600;\\n}`,\n          `.foo {\\n  background-color: blue;\\n  color: red;\\n  @apply bg-blue-600 p-4 sm:p-0;\\n}`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['prettier-plugin-organize-attributes'],\n    tests: {\n      html: [\n        [\n          `<a href=\"https://www.example.com\" class=\"sm:p-0 p-4\">Example</a>`,\n          `<a class=\"p-4 sm:p-0\" href=\"https://www.example.com\">Example</a>`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['@shopify/prettier-plugin-liquid'],\n    tests: {\n      'liquid-html': [\n        t`<a class='${yes}' href='https://www.example.com'>Example</a>`,\n        t`{% if state == true %}\\n  <a class='{{ \"${yes}\" | escape }}' href='https://www.example.com'>Example</a>\\n{% endif %}`,\n        t`{%- capture class_ordering -%}<div class=\"${yes}\"></div>{%- endcapture -%}`,\n        t`{%- capture class_ordering -%}<div class=\"foo1 ${yes}\"></div><div class=\"foo2 ${yes}\"></div>{%- endcapture -%}`,\n        t`{%- capture class_ordering -%}<div class=\"foo1 ${yes}\"><div class=\"foo2 ${yes}\"></div></div>{%- endcapture -%}`,\n        t`<p class='${yes} {{ some.prop | prepend: 'is-' }} '></p>`,\n        t`<div class='${yes} {% render 'some-snippet', settings: section.settings %}'></div>`,\n        t`<div class='${yes} {{ foo }}'></div>`,\n        t`<div class='${yes} {% render 'foo' %}'></div>`,\n        t`<div class='${yes} {% render 'foo', bar: true %}'></div>`,\n        t`<div class='${yes} {% include 'foo' %}'></div>`,\n        t`<div class='${yes} {% include 'foo', bar: true %}'></div>`,\n        t`<div class='${yes} foo--{{ id }}'></div>`,\n        t`<div class='${yes} {{ id }}'></div>`,\n\n        // Whitespace removal is disabled for Liquid\n        // due to the way Liquid prints the AST\n        // (the length of the output MUST NOT change)\n        [\n          `<div class=' sm:flex   underline  block'></div>`,\n          `<div class=' block   underline  sm:flex'></div>`,\n        ],\n        [\n          `<div class='{{ ' flex ' + ' underline ' + ' block ' }}'></div>`,\n          `<div class='{{ ' flex ' + ' underline ' + ' block ' }}'></div>`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['prettier-plugin-marko'],\n    tests: {\n      marko: [\n        t`<div class='${yes}'/>`,\n        t`<!-- <div class='${no}'/> -->`,\n        t`<div not-class='${no}'/>`,\n        t`<div class/>`,\n        t`<div class=''/>`,\n        t`<div>\n  <h1 class='${yes}'/>\n</div>`,\n        t`style {\n  h1 {\n    @apply ${yes};\n  }\n}`,\n        t`<div class=[\n  '${yes}',\n  'w-full',\n  someVariable,\n  {\n    a: true,\n  },\n  null,\n  '${yes}',\n]/>`,\n        t`<div class=['${yes}', 'underline', someVariable]/>`,\n\n        [`<div class=' sm:flex   underline  block'/>`, `<div class='block underline sm:flex'/>`],\n\n        // TODO: An improvement to the plugin would be to remove the whitespace\n        // in this scenario:\n        [\n          `<div class=[' flex ' + ' underline ' + ' block ']/>`,\n          `<div class=[' flex ' + ' underline ' + ' block ']/>`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['prettier-plugin-astro'],\n    options: {\n      tailwindAttributes: ['/(styles|classes)/'],\n    },\n    tests: {\n      astro: [\n        // ...html, // TODO:\n        [\n          '<div styles=\"sm:p-0 p-0\" classes=\"sm:p-0 p-0\" other=\"sm:p-0 p-0\"></div>',\n          '<div styles=\"p-0 sm:p-0\" classes=\"p-0 sm:p-0\" other=\"sm:p-0 p-0\"></div>',\n        ],\n\n        [\n          `{<div class=\"p-20 bg-red-100 w-full\"></div>}`,\n          `{(<div class=\"w-full bg-red-100 p-20\" />)}`,\n        ],\n        [\n          `<style>\n  h1 {\n    @apply bg-fuchsia-50 p-20 w-full;\n}\n</style>`,\n          `<style>\n  h1 {\n    @apply w-full bg-fuchsia-50 p-20;\n  }\n</style>`,\n        ],\n        t`---\nimport Layout from '../layouts/Layout.astro'\nimport Custom from '../components/Custom.astro'\n---\n\n<Layout>\n  <main class=\"${yes}\"></main>\n  <my-element class=\"${yes}\"></my-element>\n  <Custom class=\"${yes}\" />\n</Layout>`,\n        t`<div>\n  <span class:list={['${yes}', { '${yes}': '${yes}' }, new Set(['${yes}'])]}></span>\n</div>`,\n        t`<div>\n  <span class:list={[\\`${yes}\\`, \\`\\${'${yes}'}\\`, \\`\\${\\`${yes}\\`}\\`, \\`\\${\\`\\${'${yes}'}\\`}\\`]}></span>\n</div>`,\n        t`<MyReactComponent className=\"${yes}\" />`,\n        t`<MyReactComponent className={'${yes}'} />`,\n\n        [\n          `<div class=\" sm:flex   underline  block\"></div>`,\n          `<div class=\"block underline sm:flex\"></div>`,\n        ],\n        [\n          `<div class:list={[' flex ' + ' underline ' + ' block ']}></div>`,\n          `<div class:list={['flex ' + ' underline' + ' block']}></div>`,\n        ],\n      ],\n    },\n  },\n  {\n    plugins: ['prettier-plugin-svelte'],\n    tests: {\n      svelte: [\n        t`<div class=\"${yes}\" />`,\n        t`<div class />`,\n        t`<div class=\"\" />`,\n        t`<div class=\"${yes} {someVar}\" />`,\n        t`<div class=\"{someVar} ${yes}\" />`,\n        t`<div class=\"${yes} {someVar} ${yes}\" />`,\n        t`<div class={'${yes}'} />`,\n        t`<div class={'${yes}' + '${yes}'} />`,\n        t`<div class={\\`${yes}\\`} />`,\n        t`<div class={\\`${yes} \\${'${yes}' + \\`${yes}\\`} ${yes}\\`} />`,\n        t`<div class={\\`${no}\\${someVar}${no}\\`} />`,\n        t`<div class=\"${yes} {\\`${yes}\\`}\" />`,\n        t`<div let:class={clazz} class=\"${yes} {clazz}\" />`,\n        t`{#if something} <div class=\"${yes}\" /> {:else} <div class=\"${yes}\" /> {/if}`,\n        [\n          `<div class=\"sm:block uppercase flex{someVar}\" />`,\n          `<div class=\"uppercase sm:block flex{someVar}\" />`,\n        ],\n        [\n          `<div class=\"{someVar}sm:block md:inline flex\" />`,\n          `<div class=\"{someVar}sm:block flex md:inline\" />`,\n        ],\n        [\n          `<div class=\"sm:p-0 p-0 {someVar}sm:block md:inline flex\" />`,\n          `<div class=\"p-0 sm:p-0 {someVar}sm:block flex md:inline\" />`,\n        ],\n        t`{#await promise()} <div class=\"${yes}\" /> {:then} <div class=\"${yes}\" /> {/await}`,\n        t`{#await promise() then} <div class=\"${yes}\" /> {/await}`,\n\n        // Whitespace removal is applied by Svelte itself\n        [\n          `<div class=\" sm:flex   underline  block\"></div>`,\n          `<div class=\" block underline sm:flex\"></div>`,\n        ],\n\n        // Whitespace removal does not work in Svelte\n        // due to how Svelte's parser and printer work\n        // (the length of the text MUST NOT change)\n        [\n          `<div class={' flex ' + ' underline ' + ' block '}></div>`,\n          `<div class={' flex ' + ' underline ' + ' block '}></div>`,\n        ],\n\n        // Escapes\n        t`<div class={\"before:content-['\\\\\\\\2248']\"}></div>`,\n\n        // Preserve whitespace in template strings\n        // This test has lots of whitespace to ensure that the Svelte\n        // parser doesn't produce invalid syntax as output since it breaks\n        // when changing the length of the text.\n        [\n          `<div\\n class={\\`underline \\n flex\\`}></div>`,\n          `<div\\n  class={\\`flex \\n underline\\`}\\n></div>`,\n        ],\n\n        // Duplicates can be removed in simple attributes\n        [`<div class=\"flex flex underline flex flex\"></div>`, `<div class=\"flex underline\"></div>`],\n\n        // Duplicates cannot be removed in string literals otherwise invalid\n        // code will be produced during printing.\n        [`<div class={'flex underline flex'}></div>`, `<div class={'flex flex underline'}></div>`],\n\n        // Duplicates cannot be removed in template literals otherwise invalid\n        // code will be produced during printing.\n        [\n          `<div class={\\`flex underline flex\\`}></div>`,\n          `<div class={\\`flex flex underline\\`}></div>`,\n        ],\n      ],\n    },\n  },\n\n  // This test ensures that our plugin works with the multiline array, JSDoc,\n  // and import sorting plugins when used together.\n  //\n  // The plugins actually have to be *imported* in a specific order for\n  // them to function correctly *together*.\n  {\n    plugins: [\n      'prettier-plugin-multiline-arrays',\n      '@trivago/prettier-plugin-sort-imports',\n      'prettier-plugin-jsdoc',\n    ],\n    options: {\n      multilineArraysWrapThreshold: 0,\n      importOrder: ['^@one/(.*)$', '^@two/(.*)$', '^[./]'],\n      importOrderSortSpecifiers: true,\n    },\n    tests: {\n      babel: [\n        [\n          dedent`\n            import './three'\n            import '@two/file'\n            import '@one/file'\n\n            /**\n              * - Position\n              */\n            const position = {}\n            const arr = ['a', 'b', 'c', 'd', 'e', 'f']\n          `,\n          dedent`\n            import '@one/file'\n            import '@two/file'\n            import './three'\n\n            /** - Position */\n            const position = {}\n            const arr = [\n              'a',\n              'b',\n              'c',\n              'd',\n              'e',\n              'f',\n            ]\n          `,\n        ],\n      ],\n    },\n  },\n]\n\nfor (const group of tests) {\n  let name = group.plugins.join(', ')\n\n  for (let parser in group.tests) {\n    test(`parsing ${parser} works with: ${name}`, async ({ expect, skip }) => {\n      if (group.plugins.includes('prettier-plugin-multiline-arrays')) {\n        return skip(\n          'The `prettier-plugin-multiline-arrays` plugin does not work with Prettier v3.7+',\n        )\n      }\n\n      // Hide logs from Pug's prettier plugin\n      if (parser === 'pug') {\n        let pug = await import('@prettier/plugin-pug')\n        // @ts-ignore\n        pug.logger.level = 'off'\n      }\n\n      let plugins = [...group.plugins.map((name) => require.resolve(name)), pluginPath]\n\n      for (const [input, expected] of group.tests[parser]) {\n        let output = await format(input, { parser, plugins, ...group.options })\n        expect(output).toEqual(expected)\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "tests/sorter.test.ts",
    "content": "import * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { describe, expect, test } from 'vitest'\nimport { createSorter } from '../src/sorter'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\ndescribe('createSorter', () => {\n  describe('sortClassAttributes', () => {\n    test('sorts with base + relative configPath (v3)', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        filepath: path.join(fixtureDir, 'index.html'),\n        configPath: './tailwind.config.js',\n      })\n\n      let [sorted] = sorter.sortClassAttributes(['sm:bg-tomato bg-red-500'])\n      expect(sorted).toBe('bg-red-500 sm:bg-tomato')\n    })\n\n    test('sorts with base + absolute configPath', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let configPath = path.join(fixtureDir, 'tailwind.config.js')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        configPath,\n      })\n\n      let sorted = sorter.sortClassAttributes(['p-4 m-2', 'hover:text-red-500 text-blue-500'])\n      expect(sorted).toEqual(['m-2 p-4', 'text-blue-500 hover:text-red-500'])\n    })\n\n    test('sorts with v4 stylesheet', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/custom-pkg-name-v4')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        stylesheetPath: './app.css',\n      })\n\n      let [sorted] = sorter.sortClassAttributes(['sm:bg-tomato bg-red-500'])\n      expect(sorted).toBe('bg-red-500 sm:bg-tomato')\n    })\n\n    test('preserves whitespace when option is enabled', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        configPath: './tailwind.config.js',\n        preserveWhitespace: true,\n      })\n\n      let [sorted] = sorter.sortClassAttributes(['  sm:bg-tomato   bg-red-500  '])\n      expect(sorted).toBe('  bg-red-500   sm:bg-tomato  ')\n    })\n\n    test('collapses whitespace by default', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        configPath: './tailwind.config.js',\n      })\n\n      let [sorted] = sorter.sortClassAttributes(['  sm:bg-tomato   bg-red-500  '])\n      expect(sorted).toBe('bg-red-500 sm:bg-tomato')\n    })\n\n    test('removes duplicates by default', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        configPath: './tailwind.config.js',\n      })\n\n      let [sorted] = sorter.sortClassAttributes(['bg-red-500 sm:bg-tomato bg-red-500'])\n      expect(sorted).toBe('bg-red-500 sm:bg-tomato')\n    })\n\n    test('preserves duplicates when option is enabled', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        configPath: './tailwind.config.js',\n        preserveDuplicates: true,\n      })\n\n      let [sorted] = sorter.sortClassAttributes(['bg-red-500 sm:bg-tomato bg-red-500'])\n      expect(sorted).toBe('bg-red-500 bg-red-500 sm:bg-tomato')\n    })\n  })\n\n  describe('sortClassLists', () => {\n    test('sorts class lists (arrays of class names)', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        configPath: './tailwind.config.js',\n      })\n\n      let sorted = sorter.sortClassLists([\n        ['sm:bg-tomato', 'bg-red-500'],\n        ['p-4', 'm-2'],\n      ])\n\n      expect(sorted).toEqual([\n        ['bg-red-500', 'sm:bg-tomato'],\n        ['m-2', 'p-4'],\n      ])\n    })\n\n    test('removes duplicates by default', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        configPath: './tailwind.config.js',\n      })\n\n      let [sorted] = sorter.sortClassLists([['bg-red-500', 'sm:bg-tomato', 'bg-red-500']])\n\n      expect(sorted).toEqual(['bg-red-500', 'sm:bg-tomato'])\n    })\n\n    test('preserves duplicates when option is enabled', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        configPath: './tailwind.config.js',\n        preserveDuplicates: true,\n      })\n\n      let [sorted] = sorter.sortClassLists([['bg-red-500', 'sm:bg-tomato', 'bg-red-500']])\n\n      expect(sorted).toEqual(['bg-red-500', 'bg-red-500', 'sm:bg-tomato'])\n    })\n  })\n\n  describe('error handling', () => {\n    test('handles auto-detection without explicit config', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/basic')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        filepath: path.join(fixtureDir, 'index.html'),\n      })\n\n      let [sorted] = sorter.sortClassAttributes(['sm:bg-tomato bg-red-500'])\n      expect(sorted).toBe('bg-red-500 sm:bg-tomato')\n    })\n\n    test('works with no tailwind installation (uses bundled)', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/no-local-version')\n      let sorter = await createSorter({\n        base: fixtureDir,\n        stylesheetPath: './app.css',\n      })\n\n      let [sorted] = sorter.sortClassAttributes(['sm:bg-tomato bg-red-500'])\n      expect(sorted).toBe('bg-red-500 sm:bg-tomato')\n    })\n\n    test('works without a config file (uses default Tailwind config)', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/no-stylesheet-given')\n      let sorter = await createSorter({\n        base: fixtureDir,\n      })\n\n      // Should still sort using default Tailwind order\n      let [sorted] = sorter.sortClassAttributes(['p-4 m-2'])\n      expect(sorted).toBe('m-2 p-4')\n    })\n  })\n\n  describe('monorepo support', () => {\n    test('resolves tailwind relative to filepath in monorepo', async () => {\n      let fixtureDir = path.resolve(__dirname, 'fixtures/monorepo')\n      let package1Path = path.join(fixtureDir, 'package-1', 'index.html')\n\n      let sorter = await createSorter({\n        base: path.join(fixtureDir, 'package-1'),\n        filepath: package1Path,\n        stylesheetPath: './app.css',\n      })\n\n      let [sorted] = sorter.sortClassAttributes(['sm:bg-tomato bg-red-500'])\n      expect(sorted).toBe('bg-red-500 sm:bg-tomato')\n    })\n  })\n})\n"
  },
  {
    "path": "tests/tests.ts",
    "content": "import type { TestEntry } from './utils.js'\nimport { no, t, yes } from './utils.js'\n\nlet html: TestEntry[] = [\n  t`<div class=\"${yes}\"></div>`,\n  t`<!-- <div class=\"${no}\"></div> -->`,\n  t`<div class=\"${no} {{ 'p-0 sm:p-0 m-0' }}\"></div>`,\n  t`<div not-class=\"${no}\"></div>`,\n  ['<div class=\"  sm:p-0   p-0 \"></div>', '<div class=\"p-0 sm:p-0\"></div>'],\n  t`<div class></div>`,\n  t`<div class=\"\"></div>`,\n  // Ensure duplicate classes are removed\n  ['<div class=\"sm:p-0 p-0 p-0\"></div>', '<div class=\"p-0 sm:p-0\"></div>'],\n  // Duplicates are not removed for unknown classes\n  [\n    '<div class=\"idonotexist sm:p-0 p-0 idonotexist p-0 idonotexist\"></div>',\n    '<div class=\"idonotexist idonotexist idonotexist p-0 sm:p-0\"></div>',\n  ],\n  // Ensure duplicate can be kept\n  [\n    '<div class=\"sm:p-0 p-0 p-0\"></div>',\n    '<div class=\"p-0 p-0 sm:p-0\"></div>',\n    {\n      tailwindPreserveDuplicates: true,\n    },\n  ],\n\n  // … is moved to the end of the list\n  ['<div class=\"... sm:p-0 p-0\"></div>', '<div class=\"p-0 sm:p-0 ...\"></div>'],\n  ['<div class=\"… sm:p-0 p-0\"></div>', '<div class=\"p-0 sm:p-0 …\"></div>'],\n  ['<div class=\"sm:p-0 ... p-0\"></div>', '<div class=\"p-0 sm:p-0 ...\"></div>'],\n  ['<div class=\"sm:p-0 … p-0\"></div>', '<div class=\"p-0 sm:p-0 …\"></div>'],\n  ['<div class=\"sm:p-0 p-0 ...\"></div>', '<div class=\"p-0 sm:p-0 ...\"></div>'],\n  ['<div class=\"sm:p-0 p-0 …\"></div>', '<div class=\"p-0 sm:p-0 …\"></div>'],\n]\n\nlet css: TestEntry[] = [\n  t`@apply ${yes};`,\n  t`/* @apply ${no}; */`,\n  t`@not-apply ${no};`,\n  ['@apply sm:p-0\\n   p-0;', '@apply p-0\\n   sm:p-0;', { tailwindPreserveWhitespace: true }],\n\n  // Quote conversion for custom at-rules\n  [`@import \"./file.css\";`, `@import './file.css';`],\n  [`@plugin \"./file.js\";`, `@plugin './file.js';`],\n  [`@config \"./file.js\";`, `@config './file.js';`],\n  [`@source \"./file.js\";`, `@source './file.js';`],\n  [`@source not \"./file.js\";`, `@source not './file.js';`],\n  [`@source inline(\"flex\");`, `@source inline('flex');`],\n\n  [\n    `@import \"tailwindcss\";\\n\\n@import \"./theme.css\";\\n\\n@source \"./file.js\";\\n\\n.foo {\\n  color: red;\\n}`,\n    `@import 'tailwindcss';\\n\\n@import './theme.css';\\n\\n@source './file.js';\\n\\n.foo {\\n  color: red;\\n}`,\n  ],\n  [\n    `@import \"tailwindcss\";\\n\\n@import \"./theme.css\";\\n\\n@plugin \"./file.js\";\\n\\n.foo {\\n  color: red;\\n}`,\n    `@import 'tailwindcss';\\n\\n@import './theme.css';\\n\\n@plugin './file.js';\\n\\n.foo {\\n  color: red;\\n}`,\n  ],\n  [\n    `@import \"tailwindcss\";\\n\\n@import \"./theme.css\";\\n\\n@config \"./file.js\";\\n\\n.foo {\\n  color: red;\\n}`,\n    `@import 'tailwindcss';\\n\\n@import './theme.css';\\n\\n@config './file.js';\\n\\n.foo {\\n  color: red;\\n}`,\n  ],\n]\n\nexport let javascript: TestEntry[] = [\n  t`;<div class=\"${yes}\" />`,\n  t`;<div ns:class=\"${no}\" />`,\n  t`/* <div class=\"${no}\" /> */`,\n  t`// <div class=\"${no}\" />`,\n  t`;<div not-class=\"${no}\" />`,\n  t`;<div class={\\`${yes}\\`} />`,\n  t`;<div class={\\`${yes} \\${'${yes}'} \\${'${yes}' ? '${yes}' : '${yes}'}\\`} />`,\n  t`;<div class={'${yes}'} />`,\n  t`;<div class={'${yes}' + '${yes}'} />`,\n  t`;<div class={'${yes}' ? '${yes}' + '${yes}' : '${yes}'} />`,\n  t`;<div class={clsx('${yes}', ['${yes}'])} />`,\n  t`;<div class={clsx({ '${yes}': '${yes}' })} />`,\n  t`;<div class={{ '${yes}': '${yes}' }['${yes}']} />`,\n  t`;<div class />`,\n  t`;<div class=\"\" />`,\n  [\n    `;<div class={\\`sm:block inline flex\\${someVar}\\`} />`,\n    `;<div class={\\`inline sm:block flex\\${someVar}\\`} />`,\n  ],\n  [\n    `;<div class={\\`\\${someVar}sm:block md:inline flex\\`} />`,\n    `;<div class={\\`\\${someVar}sm:block flex md:inline\\`} />`,\n  ],\n  [\n    `;<div class={\\`sm:p-0 p-0 \\${someVar}sm:block md:inline flex\\`} />`,\n    `;<div class={\\`p-0 sm:p-0 \\${someVar}sm:block flex md:inline\\`} />`,\n  ],\n  [`;<div class=\"block px-1\\u3000py-2\" />`, `;<div class=\"px-1\\u3000py-2 block\" />`],\n\n  // Whitespace is normalized and duplicates are removed\n  [';<div class=\"   m-0  sm:p-0  p-0   \" />', ';<div class=\"m-0 p-0 sm:p-0\" />'],\n  [\";<div class={'   m-0  sm:p-0  p-0   '} />\", \";<div class={'m-0 p-0 sm:p-0'} />\"],\n  [';<div class={` sm:p-0\\n  p-0   `} />', ';<div class={`p-0 sm:p-0`} />'],\n  [';<div class=\"flex flex\" />', ';<div class=\"flex\" />'],\n  [';<div class={`   flex  flex `} />', ';<div class={`flex`} />'],\n  [\n    ';<div class={`   flex  flex flex${someVar}block block`} />',\n    ';<div class={`flex flex${someVar}block block`} />',\n  ],\n  [';<div class={`flex ` + `text-red-500`} />', ';<div class={`flex ` + `text-red-500`} />'],\n  [\n    ';<div class={`flex ` + `  ` + `text-red-500`} />',\n    ';<div class={`flex ` + ` ` + `text-red-500`} />',\n  ],\n\n  t`;<div class={\"before:content-['\\\\\\\\2248']\"} />`,\n  t`;<div class={\\`before:content-['\\\\\\\\2248']\\`} />`,\n  t`;<div class=\"before:content-['\\\\\\\\2248']\" />`,\n\n  [\n    `;<div class={'object-cover' + (standalone ? ' aspect-square w-full' : ' min-h-0 grow basis-0')}></div>`,\n    `;<div class={'object-cover' + (standalone ? ' aspect-square w-full' : ' min-h-0 grow basis-0')}></div>`,\n  ],\n  [\n    `;<div class=\"[&>.a\\\\_p]:after:content-['\\\\2'] [&>.a\\\\_p]:z-0\"></div>`,\n    `;<div class=\"[&>.a\\\\_p]:z-0 [&>.a\\\\_p]:after:content-['\\\\2']\"></div>`,\n  ],\n]\njavascript = javascript.concat(\n  javascript.map((test) => [\n    test[0].replace(/class/g, 'className'),\n    test[1].replace(/class/g, 'className'),\n    test[2],\n  ]),\n)\n\nlet vue: TestEntry[] = [\n  ...html,\n  t`<div :class=\"'${yes}'\"></div>`,\n  t`<!-- <div :class=\"'${no}'\"></div> -->`,\n  t`<div :class></div>`,\n  t`<div :class=\"\"></div>`,\n  t`<div :class=\"'${yes}' + '${yes}'\"></div>`,\n  t`<div :class=\"['${yes}', '${yes}']\"></div>`,\n  t`<div :class=\"[cond ? '${yes}' : '${yes}']\"></div>`,\n  t`<div :class=\"[someVar ?? '${yes}']\"></div>`,\n  t`<div :class=\"{ '${yes}': true }\"></div>`,\n  t`<div :class=\"clsx('${yes}')\"></div>`,\n  t`<div :class=\"\\`${yes}\\`\"></div>`,\n  t`<div :class=\"\\`${yes} \\${someVar}\\`\"></div>`,\n  t`<div :class=\"someVar! ? \\`${yes}\\` : \\`${yes}\\`\"></div>`, // ts\n  t`<div :class=\"someVar ? someFunc(someVar as string) + '${yes}' : ''\"></div>`, // ts\n  [\n    `<div :class=\"\\`sm:block inline flex\\${someVar}\\`\"></div>`,\n    `<div :class=\"\\`inline sm:block flex\\${someVar}\\`\"></div>`,\n  ],\n  [\n    `<div :class=\"\\`\\${someVar}sm:block md:inline flex\\`\"></div>`,\n    `<div :class=\"\\`\\${someVar}sm:block flex md:inline\\`\"></div>`,\n  ],\n  [\n    `<div :class=\"\\`sm:p-0 p-0 \\${someVar}sm:block md:inline flex\\`\"></div>`,\n    `<div :class=\"\\`p-0 sm:p-0 \\${someVar}sm:block flex md:inline\\`\"></div>`,\n  ],\n\n  [`<div :class=\"'   flex  flex '\"></div>`, `<div :class=\"'flex'\"></div>`],\n  [`<div :class=\"\\`   flex  flex \\`\"></div>`, `<div :class=\"\\`flex\\`\"></div>`],\n  [`<div :class=\"' flex ' + ' underline '\"></div>`, `<div :class=\"'flex ' + ' underline'\"></div>`],\n  [\n    `<div :class=\"' sm:p-5 ' + ' flex ' + ' underline ' + ' sm:m-5 '\"></div>`,\n    `<div :class=\"'sm:p-5 ' + ' flex' + ' underline' + ' sm:m-5'\"></div>`,\n  ],\n\n  [\n    `<div :class=\"'before:content-[\\\\'\\\\\\\\2248\\\\']'\" />`,\n    `<div :class=\"'before:content-[\\\\'\\\\\\\\2248\\\\']'\" />`,\n  ],\n]\n\nlet glimmer: TestEntry[] = [\n  t`<div class='${yes}'></div>`,\n  t`<!-- <div class='${no}'></div> -->`,\n  t`<div class='${yes} {{\"${yes}\"}}'></div>`,\n  t`<div class='${yes} {{\"${yes}\"}} ${yes}'></div>`,\n  t`<div class='${yes} {{\"${yes}\"}} {{if someVar \"${yes}\" \"${yes}\"}}'></div>`,\n  t`<div class='${yes} {{\"${yes}\"}} {{if someVar \"${yes}\" \"${yes}\"}}' {{if someVar \"attr='${no}'\" \"attr='${no}'\"}}></div>`,\n  [\n    `<div class='md:inline flex sm:block{{someVar}}'></div>`,\n    `<div class='flex md:inline sm:block{{someVar}}'></div>`,\n  ],\n  [\n    `<div class='sm:p-0 p-0 {{someVar}}sm:block md:inline flex'></div>`,\n    `<div class='p-0 sm:p-0 {{someVar}}sm:block flex md:inline'></div>`,\n  ],\n  t`<div not-class='${no}'></div>`,\n  [\"<div class='  sm:p-0   p-0 '></div>\", \"<div class='p-0 sm:p-0'></div>\"],\n  t`<div class></div>`,\n  t`<div class=''></div>`,\n  t`{{link 'Some page' href=person.url class='${no}'}}`,\n  t`<div class='{{if @isTrue (concat \"border-l-4 border-\" @borderColor)}}'></div>`,\n  [\n    `<div class='{{if @isTrue (concat \"border-l-4 border-x-4 border-\" @borderColor)}}'></div>`,\n    `<div class='{{if @isTrue (concat \"border-x-4 border-l-4 border-\" @borderColor)}}'></div>`,\n  ],\n  [\n    `<div class='{{if @isTrue (concat \"border-l-4 border \" @borderColor)}}'></div>`,\n    `<div class='{{if @isTrue (concat \"border border-l-4 \" @borderColor)}}'></div>`,\n  ],\n  [\n    `<div class='{{if @isTrue (nope \"border-l-4 border-\" @borderColor)}}'></div>`,\n    `<div class='{{if @isTrue (nope \"border- border-l-4\" @borderColor)}}'></div>`,\n  ],\n\n  [`<div class='flex  flex '></div>`, `<div class='flex'></div>`],\n\n  [\n    `<div class='sm:p-0   p-0  p-0 {{someVar}}sm:block flex md:inline   flex '></div>`,\n    `<div class='p-0 sm:p-0 {{someVar}}sm:block flex md:inline'></div>`,\n  ],\n]\n\nexport let tests: Record<string, TestEntry[]> = {\n  html,\n  glimmer,\n  lwc: html,\n  vue: [\n    //\n    ...vue,\n    t`<div :class=\"\\`${yes} \\${someVar} ${yes} \\${'${yes}'}\\`\"></div>`,\n  ],\n  angular: [\n    ...html,\n    t`<div [ngClass]=\"'${yes}'\"></div>`,\n    t`<!-- <div [ngClass]=\"'${no}'\"></div> -->`,\n    t`<div [ngClass]></div>`,\n    t`<div [ngClass]=\"\"></div>`,\n    t`<div [ngClass]=\"'${yes}' + '${yes}'\"></div>`,\n    t`<div [ngClass]=\"['${yes}', '${yes}']\"></div>`,\n    t`<div [ngClass]=\"[cond ? '${yes}' : '${yes}']\"></div>`,\n    t`<div [ngClass]=\"[someVar ?? '${yes}']\"></div>`,\n    t`<div [ngClass]=\"{ '${yes}': true }\"></div>`,\n    t`<div [ngClass]=\"clsx('${yes}')\"></div>`,\n    t`<div [ngClass]=\"{ '${yes}': (some.thing | urlPipe: { option: true } | async), '${yes}': true }\"></div>`,\n    t`<div [ngClass]=\"{ '${yes}': foo && bar?.['baz'] }\" class=\"${yes}\"></div>`,\n\n    [\n      `<div [ngClass]=\"' flex ' + ' italic      underline ' + ' block '\"></div>`,\n      `<div [ngClass]=\"'flex ' + ' italic underline ' + ' block'\"></div>`,\n    ],\n\n    // TODO: Enable this test — it causes console noise but not a failure\n    // t`<div [ngClass]=\"{ '${no}': foo && definitely&a:syntax*error }\" class=\"${yes}\"></div>`,\n\n    t`<div [ngClass]=\"\\`${yes}\\`\"></div>`,\n  ],\n  css: [\n    //\n    ...css,\n    t`@apply ${yes} !important;`,\n  ],\n  scss: [\n    ...css,\n    t`@apply ${yes} #{!important};`,\n    t`@apply ${yes} #{'!important'};`,\n    t`@apply ${yes} #{\"!important\"};`,\n\n    // These shouldn't ever be used but they are valid\n    // syntax so we might as well not break them\n    t`@apply ${yes} #{\"\"!important\"\"};`,\n    t`@apply ${yes} #{'''!important'''};`,\n    t`@apply ${yes} #{\"'\"'\"!important\"'\"'\"};`,\n  ],\n  less: [\n    //\n    ...css,\n    t`@apply ${yes} !important;`,\n    t`@apply ~\"${yes}\";`,\n    t`@apply ~'${yes}';`,\n  ],\n  babel: javascript,\n  typescript: javascript,\n  'babel-ts': javascript,\n  flow: javascript,\n  'babel-flow': javascript,\n  acorn: javascript,\n  meriyah: javascript,\n  mdx: javascript\n    .filter((test) => !test[0].startsWith('/*') && !test[1].startsWith('/*'))\n    .map((test) => [test[0].replace(/^;/, ''), test[1].replace(/^;/, ''), test[2]]),\n}\n"
  },
  {
    "path": "tests/utils.ts",
    "content": "import * as path from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport * as prettier from 'prettier'\n\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = path.dirname(__filename)\n\nlet testClassName = 'sm:p-0 p-0'\nlet testClassNameSorted = 'p-0 sm:p-0'\n\nexport let yes = '__YES__'\nexport let no = '__NO__'\n\nexport type TestEntry = [\n  input: string,\n  output: string,\n  options?: {\n    tailwindPreserveWhitespace?: boolean\n    tailwindPreserveDuplicates?: boolean\n  },\n]\n\nexport function t(strings: TemplateStringsArray, ...values: string[]): TestEntry {\n  let input = ''\n  strings.forEach((string, i) => {\n    input += string + (values[i] ? testClassName : '')\n  })\n\n  let output = ''\n  strings.forEach((string, i) => {\n    let value = values[i] || ''\n    if (value === yes) value = testClassNameSorted\n    else if (value === no) value = testClassName\n    output += string + value\n  })\n\n  return [input, output, { tailwindPreserveWhitespace: true }]\n}\n\nexport let pluginPath = path.resolve(__dirname, '../dist/index.mjs')\n\nexport async function format(str: string, options: prettier.Options = {}) {\n  let result = await prettier.format(str, {\n    semi: false,\n    singleQuote: true,\n    printWidth: 9999,\n    parser: 'html',\n    ...options,\n    plugins: [\n      //\n      ...(options.plugins ?? []),\n      // plugin,\n      pluginPath,\n    ],\n  })\n\n  return result.trim()\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"lib\": [\"ESNext\", \"DOM\"],\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleDetection\": \"force\",\n    \"allowJs\": true,\n\n    \"moduleResolution\": \"Bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"verbatimModuleSyntax\": true,\n    \"isolatedModules\": true,\n    \"preserveConstEnums\": true,\n    \"noEmit\": true,\n\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"forceConsistentCasingInFileNames\": true  ,\n    \n    \"stripInternal\": true,\n  },\n  \"include\": [\"src/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "tsdown.config.ts",
    "content": "import { readFile } from 'node:fs/promises'\nimport * as path from 'node:path'\nimport { defineConfig, Rolldown } from 'tsdown'\n\n/**\n * Patches jiti to use require for babel import.\n */\nfunction patchJiti(): Rolldown.Plugin {\n  return {\n    name: 'patch-jiti',\n    async load(id) {\n      if (!/jiti[\\\\/]lib[\\\\/]jiti\\.mjs$/.test(id)) {\n        return null\n      }\n\n      let original = await readFile(id, 'utf8')\n      return {\n        code: original.replace(\n          'createRequire(import.meta.url)(\"../dist/babel.cjs\")',\n          'require(\"../dist/babel.cjs\")',\n        ),\n      }\n    },\n  }\n}\n\n/**\n * Inlines CSS imports as JavaScript strings.\n */\nfunction inlineCssImports(): Rolldown.Plugin {\n  return {\n    name: 'inline-css-imports',\n    async load(id) {\n      // Inline CSS imports\n      if (id.endsWith('.css')) {\n        let content = await readFile(id, 'utf-8')\n        return {\n          code: `export default ${JSON.stringify(content)}`,\n          moduleType: 'js',\n        }\n      }\n\n      // Inline preflight in v3\n      if (id.endsWith('corePlugins.js')) {\n        let preflightPath = path.resolve(path.dirname(id), './css/preflight.css')\n        let preflightContent = await readFile(preflightPath, 'utf-8')\n        let content = await readFile(id, 'utf-8')\n\n        // This is a bit fragile but this is to inline preflight for the\n        // *bundled* version which means a failing test should be enough\n        content = content.replace(\n          `_fs.default.readFileSync(_path.join(__dirname, \"./css/preflight.css\"), \"utf8\")`,\n          JSON.stringify(preflightContent),\n        )\n\n        return {\n          code: content,\n        }\n      }\n\n      return null\n    },\n  }\n}\n\nexport default defineConfig({\n  entry: ['./src/index.ts', './src/sorter.ts'],\n  format: 'esm',\n  platform: 'node',\n  target: 'node14.21.3',\n  external: ['prettier'],\n  dts: true,\n  sourcemap: false,\n  fixedExtension: true,\n  minify: 'dce-only',\n  inlineOnly: false,\n  shims: true,\n  plugins: [patchJiti(), inlineCssImports()],\n})\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n  test: {\n    testTimeout: 10000,\n    css: true,\n  },\n\n  plugins: [\n    {\n      name: 'force-inline-css',\n      enforce: 'pre',\n      resolveId(id) {\n        if (!id.endsWith('.css')) return\n        if (id.includes('?raw')) return\n        return this.resolve(`${id}?raw`)\n      },\n    },\n  ],\n})\n"
  }
]