@culur/prettier-plugin-tailwindcss-group 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +558 -0
  4. package/dist/index.d.mts +673 -0
  5. package/dist/index.mjs +4194 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +66 -0
  8. package/src/__tests__/comments.test.ts +171 -0
  9. package/src/__tests__/plugin-official.test.ts +32 -0
  10. package/src/__tests__/readme.test.ts +13 -0
  11. package/src/__tests__/utils/ast-estree.ts +22 -0
  12. package/src/__tests__/utils/ast-md.ts +101 -0
  13. package/src/__tests__/utils/test-format.ts +35 -0
  14. package/src/ast/ast-array-expression.test.ts +33 -0
  15. package/src/ast/ast-array-expression.ts +20 -0
  16. package/src/ast/ast-call-expression.test.ts +17 -0
  17. package/src/ast/ast-call-expression.ts +5 -0
  18. package/src/ast/ast-comment.test.ts +45 -0
  19. package/src/ast/ast-comment.ts +25 -0
  20. package/src/ast/ast-common.ts +29 -0
  21. package/src/ast/ast-jsx.test.ts +56 -0
  22. package/src/ast/ast-jsx.ts +53 -0
  23. package/src/ast/ast-literal.test.ts +47 -0
  24. package/src/ast/ast-literal.ts +17 -0
  25. package/src/ast/ast-property.test.ts +96 -0
  26. package/src/ast/ast-property.ts +32 -0
  27. package/src/ast/index.ts +8 -0
  28. package/src/ast/key.test.ts +21 -0
  29. package/src/ast/key.ts +4 -0
  30. package/src/group-classes/group-classes.bucket.ts +61 -0
  31. package/src/group-classes/group-classes.format.ts +39 -0
  32. package/src/group-classes/group-classes.test.ts +126 -0
  33. package/src/group-classes/group-classes.ts +34 -0
  34. package/src/group-classes/group-classes.types.ts +5 -0
  35. package/src/handle-node/handle-cva-class-value.test.ts +133 -0
  36. package/src/handle-node/handle-cva-class-value.ts +47 -0
  37. package/src/handle-node/handle-expressions.test.ts +140 -0
  38. package/src/handle-node/handle-expressions.ts +63 -0
  39. package/src/handle-node/handle-string-literal.test.ts +135 -0
  40. package/src/handle-node/handle-string-literal.ts +53 -0
  41. package/src/handle-targets/index.ts +3 -0
  42. package/src/handle-targets/target-cva-base.test.ts +103 -0
  43. package/src/handle-targets/target-cva-base.ts +17 -0
  44. package/src/handle-targets/target-cva-compound-variants.test.ts +239 -0
  45. package/src/handle-targets/target-cva-compound-variants.ts +45 -0
  46. package/src/handle-targets/target-cva-variants.test.ts +178 -0
  47. package/src/handle-targets/target-cva-variants.ts +50 -0
  48. package/src/handle-targets/target-cva.test.ts +126 -0
  49. package/src/handle-targets/target-cva.ts +33 -0
  50. package/src/handle-targets/target-jsx.test.ts +242 -0
  51. package/src/handle-targets/target-jsx.ts +65 -0
  52. package/src/handle-targets/target-object-properties.test.ts +140 -0
  53. package/src/handle-targets/target-object-properties.ts +41 -0
  54. package/src/index.ts +13 -0
  55. package/src/options.test.ts +73 -0
  56. package/src/options.ts +94 -0
  57. package/src/orders/class-groups.test.ts +74 -0
  58. package/src/orders/class-groups.ts +178 -0
  59. package/src/orders/class-order.tailwind-merge.ts +26 -0
  60. package/src/orders/modifier-order.prettier-plugin-tailwindcss.test.ts +33 -0
  61. package/src/orders/modifier-order.prettier-plugin-tailwindcss.ts +23 -0
  62. package/src/prettier.d.ts +21 -0
  63. package/src/printers.test.ts +58 -0
  64. package/src/printers.ts +29 -0
  65. package/src/printers.walk-ast.test.ts +58 -0
  66. package/src/printers.walk-ast.ts +41 -0
  67. package/src/utils/split-classes.ts +3 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # @culur/prettier-plugin-tailwindcss-group
2
+
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 🎉 Initialize `@culur/prettier-plugin-tailwindcss-group` Prettier plugin ([`5c7e5f6`](https://github.com/culur/culur/commit/5c7e5f65ea5b15cee03464ad3d5b1b4982c3dbbc)) ([@phamhongphuc](https://github.com/phamhongphuc)).
8
+
9
+ - **Intelligent Tailwind CSS Class Grouping**: Automatically splits and groups long utility class strings into 14 logical CSS categories (Layout, Typography, Backgrounds, Borders, Effects, Transitions, Interactivity, SVG, etc.) based on `tailwind-merge` class groups and sorted by `prettier-plugin-tailwindcss` compiler order.
10
+ - **Category & Modifier Threshold Pipeline**: Separates base classes and modifier variants within each category, keeping compact variants on the same line (`≤ modifierThreshold`) or splitting into dedicated variant clusters (`> modifierThreshold`).
11
+ - **Comprehensive Target Support**:
12
+ - **JSX/TSX Attributes**: Formats `className` and custom attributes ending with `ClassName` / `className` (e.g. `wrapperClassName`, `containerClassName`).
13
+ - **Object Properties**: Formats helper calls within objects (e.g. `{ className: cn(...) }`), preserving the function call wrapper.
14
+ - **Class Variance Authority (`cva`)**: Formats base classes, variant options, and compound variant definitions (`class` / `className`) across string, array, and function formats.
15
+ - **Short String Simplification**: Automatically simplifies redundant single-line expressions (e.g. `{"..."}`, `['...']`, or `cn('...')`) back to plain static strings when under `classNameThreshold`.
16
+ - **Dynamic Expression Organization**: Reorganizes static class strings to the front while preserving dynamic expressions, conditionals, and object arguments in `cn(...)`.
17
+ - **Comment Preservation (Bail-Out Strategy)**: Safely detects and preserves code comments (`//` and `/* ... */`) by bailing out on commented nodes, avoiding Prettier `"Comment was not printed"` errors.
18
+ - **Configurable Options**:
19
+ - `classNameThreshold`: Threshold of class names to trigger wrapping (default: `10`).
20
+ - `modifierThreshold`: Threshold of modifier classes per category before splitting into separate lines (default: `2`).
21
+ - `tailwindFunctions`: List of custom helper function names to format (default: `['cn']`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Culur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,558 @@
1
+ # `@culur/prettier-plugin-tailwindcss-group`
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@culur/prettier-plugin-tailwindcss-group?logo=npm)](https://www.npmjs.com/package/@culur/prettier-plugin-tailwindcss-group)
4
+ [![NPM Download](https://img.shields.io/npm/dm/@culur/prettier-plugin-tailwindcss-group?logo=npm)](https://www.npmjs.com/package/@culur/prettier-plugin-tailwindcss-group)
5
+ [![NPM License](https://img.shields.io/npm/l/@culur/prettier-plugin-tailwindcss-group)](../../LICENSE)
6
+
7
+ [![CodeFactor](https://www.codefactor.io/repository/github/culur/culur/badge)](https://www.codefactor.io/repository/github/culur/culur)
8
+ [![Codecov](https://img.shields.io/codecov/c/github/culur/culur)](https://app.codecov.io/gh/culur/culur)
9
+ [![Build and release](https://github.com/culur/culur/actions/workflows/build-and-release.yml/badge.svg)](https://github.com/culur/culur/actions/workflows/build-and-release.yml)
10
+
11
+ > Prettier plugin to automatically group, format, and simplify Tailwind CSS class names across JSX attributes, object properties, and CVA variant definitions.
12
+
13
+ ## ✨ Features
14
+
15
+ This plugin enforces a clean and consistent structure for Tailwind CSS classes. It provides:
16
+
17
+ - **Automatic Grouping:** Automatically breaks down long class strings into logically grouped arguments by utility category (Layout, Typography, Visuals, etc.).
18
+ - **Multi-Target Support:** Formats JSX attributes (`className`, `wrapperClassName`), object properties (`className`, `wrapperclassName`), and `cva()` parameters from `class-variance-authority` (commonly used in `shadcn/ui`).
19
+ - **Redundancy Simplification:** Flattens unnecessary wrapper functions into pure strings if they are under the threshold.
20
+ - **Comment Preservation:** Safely detects and preserves code comments (`//` and `/* ... */`) by bailing out on commented nodes to avoid Prettier `"Comment was not printed"` errors.
21
+ - **Idempotency & Compatibility:** Strictly manipulates the Prettier AST. The output is fully compliant with standard Prettier formatting and integrates flawlessly with `prettier-plugin-tailwindcss`.
22
+
23
+ ## 💿 Installation
24
+
25
+ Add `@culur/prettier-plugin-tailwindcss-group` and `prettier-plugin-tailwindcss` to your project. `prettier-plugin-tailwindcss` is a required peer dependency used to resolve official Tailwind CSS compiler sorting and variant hierarchies.
26
+
27
+ > [!NOTE]
28
+ > **No need to install `tailwind-merge`:** The required `tailwind-merge` engine is already bundled directly inside `@culur/prettier-plugin-tailwindcss-group`. You do **not** need to install `tailwind-merge` as a separate dependency unless your application code directly imports utility functions like `twMerge()` or `cn()`.
29
+
30
+ ```bash
31
+ # Using npm
32
+ npm install @culur/prettier-plugin-tailwindcss-group prettier-plugin-tailwindcss --save-dev
33
+
34
+ # Using pnpm
35
+ pnpm install @culur/prettier-plugin-tailwindcss-group prettier-plugin-tailwindcss --save-dev
36
+
37
+ # Using yarn
38
+ yarn add @culur/prettier-plugin-tailwindcss-group prettier-plugin-tailwindcss --dev
39
+
40
+ # Using bun
41
+ bun add @culur/prettier-plugin-tailwindcss-group prettier-plugin-tailwindcss --dev
42
+ ```
43
+
44
+ _Note: You also need `prettier` (>=3.0.0) installed in your project._
45
+
46
+ ## 📖 Usage & Integration
47
+
48
+ Add to your `.prettierrc` configuration:
49
+
50
+ ```json
51
+ {
52
+ "plugins": [
53
+ "@culur/prettier-plugin-tailwindcss-group",
54
+ "prettier-plugin-tailwindcss"
55
+ ],
56
+ "classNameThreshold": 10,
57
+ "modifierThreshold": 2,
58
+ "tailwindFunctions": ["cn"]
59
+ }
60
+ ```
61
+
62
+ Both plugins share the `tailwindFunctions` configuration option, so helper functions like `cn()` or `clsx()` are sorted and grouped seamlessly.
63
+
64
+ ## ⚙️ Options
65
+
66
+ | Option | Type | Default | Description |
67
+ | -------------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `classNameThreshold` | `int` | `10` | Threshold of class names (split by whitespace) to trigger wrapping. |
69
+ | `modifierThreshold` | `int` | `2` | Threshold of modifier/variant classes within a category before splitting into separate lines. |
70
+ | `tailwindFunctions` | `string[]` | `["cn"]` | List of helper function names. The first element is used as default wrapper function. **Note:** `tailwindFunctions` is also an official option of `prettier-plugin-tailwindcss`. |
71
+
72
+ ## 📐 Formatting Rules & Concepts
73
+
74
+ This plugin inspects AST nodes to find valid styling targets and applies formatting rules.
75
+
76
+ ### 🔠 Representation Modes
77
+
78
+ Depending on the target context and rule thresholds, class strings are represented in one of the following AST forms:
79
+
80
+ - **StringLiteral:** A raw static string literal `"class-1 class-2"`.
81
+
82
+ ```tsx
83
+ // JSX Attribute
84
+ <div className="flex items-center" />;
85
+
86
+ // CVA Parameter
87
+ const button = cva('flex items-center');
88
+ ```
89
+
90
+ - **ExpressionStringLiteral:** A JSX expression container wrapping a single string literal `{"class-1 class-2"}` (JSX Attributes only).
91
+
92
+ ```tsx
93
+ <div className={'flex items-center'} />
94
+ ```
95
+
96
+ - **ExpressionCall:** A helper function call such as `{cn('class-1 class-2')}` (JSX Attributes) or `cn('class-1 class-2')` (Object Properties / CVA Parameters).
97
+
98
+ ```tsx
99
+ // JSX Attribute Expression Call
100
+ <div className={cn('flex items-center')} />;
101
+
102
+ // Object Property Expression Call
103
+ const config = { className: cn('flex items-center') };
104
+ ```
105
+
106
+ - **ExpressionArrayString:** An array literal containing string items `['class-1', 'class-2']` (used in CVA parameters).
107
+
108
+ ```ts
109
+ const button = cva([
110
+ 'flex items-center justify-center',
111
+ 'bg-white shadow-sm',
112
+ ]);
113
+ ```
114
+
115
+ ---
116
+
117
+ ### 🎯 Supported Targets
118
+
119
+ The plugin automatically identifies and formats the following targets in your codebase:
120
+
121
+ - **JSX Attributes:** Attributes named `className` or ending with a case-sensitive suffix `ClassName` (e.g. `wrapperClassName`, `containerClassName`, `headerClassName`).
122
+ - **Attribute Pattern Matching:** Matched using strict suffix matching (`ends with 'ClassName'`). Exact names like `className`, `wrapperClassName`, and `containerClassName` match, whereas uppercase variants like `CLASSNAME` or unrelated attributes like `class` / `id` are ignored.
123
+ - **Bypassed Cases:** Attributes with no value (e.g. `<div className />`) or empty strings (`className=""`) are safely bypassed without transformation.
124
+ - **Generated Modes:** **StringLiteral** or **ExpressionCall** based on the class count threshold.
125
+
126
+ ```tsx
127
+ <button
128
+ className="flex items-center p-4"
129
+ containerClassName="bg-white shadow-sm"
130
+ />
131
+ ```
132
+
133
+ - **Object Properties:** Static properties named `className` or ending with `<prefix>ClassName` / `<prefix>className` (case-sensitive, e.g., `wrapperClassName`, `innerclassName`).
134
+ - **Generated Modes:** **ExpressionCall** only.
135
+
136
+ > **Note:** Formatted **only** when calling a helper function in `tailwindFunctions` (e.g. `cn(...)`) to avoid false positives on non-CSS properties. The `ExpressionCall` wrapper is intentionally preserved (never simplified to `StringLiteral`) so you can freely add classes later without manual re-wrapping, and sorting plugins continue to work seamlessly. Computed keys are not supported.
137
+
138
+ ```ts
139
+ const styles = {
140
+ // Formatted because it calls cn(...)
141
+ className: cn(
142
+ 'flex items-center text-sm font-bold',
143
+ isActive && 'bg-blue-500',
144
+ ),
145
+ wrapperClassName: cn('bg-white shadow-sm'),
146
+ };
147
+ ```
148
+
149
+ - **CVA Function Calls:** Arguments inside `cva(...)` functions from `class-variance-authority` (commonly used in libraries like `shadcn/ui`):
150
+ - Base classes: `params[0]`
151
+ - Variant classes: `params[1]['variants'][<variant_name>][<option_name>]`
152
+ - Compound variant classes: `params[1]['compoundVariants'][<index>]['class']` or `['className']`
153
+ - **Ignored / Non-Class Keys:** Configuration objects like `defaultVariants` (which define variant option identifiers) and condition selector keys inside `compoundVariants` (such as `intent: 'primary'`, `size: 'small'`) are non-class properties and are strictly left untouched.
154
+ - **Spread Elements:** When `cva` is called with a spread config (e.g. `cva(base, ...spreadConfig)`), the plugin formats `params[0]` (base classes) and safely skips `...spreadConfig` since dynamic objects cannot be statically grouped at format time.
155
+ - **Generated Modes:** **StringLiteral**, **ExpressionArrayString**, or **ExpressionCall** based on the input node type and class count threshold.
156
+
157
+ > **Note:** Matches direct `cva(...)` function calls. Aliased imports (e.g. `import { cva as createVariants }`) are intentionally not tracked to maintain high performance and simplicity without requiring complex AST scope analysis.
158
+
159
+ ```ts
160
+ import { cva } from 'class-variance-authority';
161
+
162
+ const buttonVariants = cva(
163
+ 'flex items-center justify-center rounded-md text-sm font-medium', // params[0]
164
+ {
165
+ variants: {
166
+ variant: {
167
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90', // params[1]['variants']['variant']['default']
168
+ outline: ['border border-input bg-background hover:bg-accent'],
169
+ },
170
+ },
171
+ compoundVariants: [
172
+ {
173
+ intent: 'primary',
174
+ disabled: false,
175
+ class: 'hover:bg-blue-600', // params[1]['compoundVariants'][0]['class']
176
+ },
177
+ ],
178
+ defaultVariants: {
179
+ intent: 'primary',
180
+ size: 'medium',
181
+ disabled: false,
182
+ },
183
+ },
184
+ );
185
+ ```
186
+
187
+ ---
188
+
189
+ ### ⚡ Core Rules
190
+
191
+ #### 1. Short String Simplification (`≤ classNameThreshold`)
192
+
193
+ Targets with class count `≤ classNameThreshold` are simplified back to **StringLiteral** format. Redundant expressions like `{"..."}`, `['...']`, or `cn('...')` containing a single static string under the threshold are simplified to plain strings.
194
+
195
+ <!-- test-case: rule-1-short-strings-jsx-expression -->
196
+
197
+ **Before:**
198
+
199
+ ```jsx
200
+ <div className={'flex items-center'} />
201
+ ```
202
+
203
+ **After:**
204
+
205
+ ```jsx
206
+ <div className="flex items-center" />
207
+ ```
208
+
209
+ <!-- test-case: rule-1-short-strings-function-call -->
210
+
211
+ **Before:**
212
+
213
+ ```jsx
214
+ <div className={cn('flex', 'items-center')} />
215
+ ```
216
+
217
+ **After:**
218
+
219
+ ```jsx
220
+ <div className="flex items-center" />
221
+ ```
222
+
223
+ ---
224
+
225
+ #### 2. Automatic Grouping (`> classNameThreshold`)
226
+
227
+ When static classes exceed `classNameThreshold`, the string is automatically converted to grouped arguments based on target context:
228
+
229
+ - **JSX Attributes & Object Properties:** Converted to **ExpressionCall** format (`cn(...)`).
230
+ - **CVA Parameters (`params[0]`, `variants`, `compoundVariants`):** Converted to **ExpressionArrayString** format (`['group-1', 'group-2']`) for strings/arrays, or preserved as **ExpressionCall** (`cn('group-1', 'group-2', ...)`) with grouped arguments.
231
+
232
+ <!-- test-case: rule-2-automatic-grouping classNameThreshold=4 -->
233
+
234
+ **Before:**
235
+
236
+ ```jsx
237
+ <div className="flex items-center justify-center text-sm font-bold bg-white shadow-md border-gray-200" />
238
+ ```
239
+
240
+ **After:**
241
+
242
+ ```jsx
243
+ <div
244
+ className={cn(
245
+ 'flex items-center justify-center',
246
+ 'text-sm font-bold',
247
+ 'bg-white',
248
+ 'border-gray-200 shadow-md',
249
+ )}
250
+ />
251
+ ```
252
+
253
+ ---
254
+
255
+ #### 3. Argument Organization & CVA Handling
256
+
257
+ - **JSX Attributes & Object Properties:** In **ExpressionCall** formats like `cn(...)` containing dynamic arguments (conditionals, variables, objects), static class strings are grouped and moved to the **front**, while dynamic arguments remain at the **end**.
258
+ - **Supported Wrapper Argument Structure:**
259
+ The plugin optimizes **flat argument lists** of the form `cn(string, string, dynamicExpression, ...)`. It does **not** unpack or recursively extract static class strings from complex nested static structures supported by libraries like `clsx`:
260
+
261
+ ```ts
262
+ // ❌ Complex / nested array syntax from clsx is NOT unpacked:
263
+ clsx(['foo', 0, false, 'bar']);
264
+ clsx(['foo'], ['', 0, false, 'bar'], [['baz', [['hello'], 'there']]]);
265
+ ```
266
+
267
+ Any argument that is not a direct static `StringLiteral` (such as an array literal `[...]` or object literal `{...}` passed into `cn(...)`) is treated as a dynamic argument and preserved at the end of the argument list.
268
+
269
+ - **CVA Parameters (`params[0]`, `variants`, `compoundVariants`):**
270
+ CVA class values are transformed based on their node type:
271
+ - **StringLiteral:**
272
+ - `≤ classNameThreshold` ➔ **StringLiteral** (`"class-1 class-2"`)
273
+ - `> classNameThreshold` ➔ **ExpressionArrayString** (`['group-1', 'group-2']`)
274
+ - **ExpressionArrayString (`['...']`):**
275
+ - `≤ classNameThreshold` ➔ **StringLiteral** (`"class-1 class-2"`) (if all static)
276
+ - `> classNameThreshold` ➔ **ExpressionArrayString** (`['group-1', 'group-2']`) (static strings grouped)
277
+ - **ExpressionCall (`cn(...)`):**
278
+ - `≤ classNameThreshold` ➔ **StringLiteral** (`"class-1 class-2"`) (if all static)
279
+ - `> classNameThreshold` ➔ **ExpressionCall** (`cn('group-1', 'group-2', ...)`) (arguments grouped and organized)
280
+ - **Dynamic Expressions** (e.g. `[isActive && 'bg-blue-500']` without static strings) ➔ **Preserved**.
281
+
282
+ > [!WARNING]
283
+ > **Warning on `tailwind-merge` class precedence:** Moving static string arguments to the front changes argument evaluation order in `cn()` / `clsx()`. If you rely on `tailwind-merge` to resolve overlapping classes between static strings and dynamic expressions, dynamic arguments placed at the end will now override static classes placed at the front. Ensure there are no conflicting Tailwind utility classes between static and dynamic arguments.
284
+
285
+ <!-- test-case: rule-3-argument-organization classNameThreshold=4 -->
286
+
287
+ **Before:**
288
+
289
+ ```tsx
290
+ <div
291
+ className={cn(
292
+ isActive && 'bg-blue-500',
293
+ 'flex items-center text-sm font-bold',
294
+ { 'opacity-50': disabled },
295
+ 'p-4',
296
+ )}
297
+ />
298
+ ```
299
+
300
+ **After:**
301
+
302
+ ```tsx
303
+ <div
304
+ className={cn(
305
+ 'flex items-center',
306
+ 'p-4',
307
+ 'text-sm font-bold',
308
+ isActive && 'bg-blue-500',
309
+ { 'opacity-50': disabled },
310
+ )}
311
+ />
312
+ ```
313
+
314
+ ---
315
+
316
+ #### 4. Comment Preservation (Bail-Out Strategy)
317
+
318
+ To prevent code corruption and avoid Prettier's `"Comment was not printed"` syntax errors, the plugin adopts a strict **Bail-Out Strategy** whenever comments (`//` or `/* ... */`) are detected inside class expressions or arguments being formatted:
319
+
320
+ - **Bail-Out Behavior:** If any static string literal or function argument contains attached leading, trailing, or inline comments (e.g. inside `className={cn(...)}`, `className=/* ... */"..."`, or `cva([...])`), the plugin immediately **skips transformation** for that target.
321
+ - **Comment Integrity:** The original AST structure is preserved untouched, ensuring Prettier and `prettier-plugin-tailwindcss` can safely print all comments in their exact intended positions.
322
+ - **Outside Comments:** Comments placed outside the formatted arguments (e.g. above JSX attributes or component declarations) do not trigger a bail-out and continue to format normally.
323
+
324
+ <!-- test-case: rule-4-comment-preservation -->
325
+
326
+ **Before:**
327
+
328
+ ```tsx
329
+ <div
330
+ className={cn(
331
+ // Inner layout comment
332
+ 'flex items-center', // Trailing comment
333
+ 'bg-red-500 text-white font-bold p-4 shadow-md',
334
+ )}
335
+ />
336
+ ```
337
+
338
+ **After (Preserved):**
339
+
340
+ ```tsx
341
+ <div
342
+ className={cn(
343
+ // Inner layout comment
344
+ 'flex items-center', // Trailing comment
345
+ 'bg-red-500 p-4 font-bold text-white shadow-md',
346
+ )}
347
+ />
348
+ ```
349
+
350
+ ## 🏗️ Core Architecture & Engine Reuse
351
+
352
+ Under the hood, `@culur/prettier-plugin-tailwindcss-group` coordinates two battle-tested engines to provide zero-guesswork, 100% specification-compliant grouping and sorting:
353
+
354
+ ```txt
355
+ Input Classes
356
+ │
357
+ ┌─────────────────────┴─────────────────────┐
358
+ ▼ ▼
359
+ tailwind-merge (Parser Engine) prettier-plugin-tailwindcss
360
+ ┌───────────────────────────────────┐ ┌─────────────────────────────────────┐
361
+ │ • Resolves base ClassGroup IDs │ │ • Official Tailwind Compiler Engine │
362
+ │ • Strips postfix modifiers (/50) │ │ • AST Stylesheet order evaluation │
363
+ │ • Maps classes to 14 Categories │ │ • Sorts modifiers (hover, dark, sm) │
364
+ └─────────────────┬─────────────────┘ └──────────────────┬──────────────────┘
365
+ │ │
366
+ └────────────────────┬───────────────────────┘
367
+ ▼
368
+ Category-First Modifier Pipeline
369
+ (Threshold Rule: modifierThreshold = 2)
370
+ │
371
+ ┌────────────────┴────────────────┐
372
+ ▼ ▼
373
+ Total Modifiers ≤ 2 Total Modifiers > 2
374
+ ┌───────────────────────────┐ ┌─────────────────────────────┐
375
+ │ Merge into a single line │ │ Line 1: Base classes │
376
+ │ with base classes. │ │ Line 2..N: Modifier groups │
377
+ └───────────────────────────┘ └─────────────────────────────┘
378
+ ```
379
+
380
+ 1. **`tailwind-merge`**: Parses class groups, strips postfix modifiers (such as `group/button` or opacity modifiers `bg-red-500/50`), and reliably classifies utilities into corresponding CSS categories.
381
+ 2. **`prettier-plugin-tailwindcss`**: Leverages the official Tailwind CSS Compiler AST order to sort all modifiers and variants (`group-hover`, `hover`, `focus`, `dark`, `sm`, `md`, `@container`, etc.) strictly according to Tailwind CSS specificity standards.
382
+
383
+ ### 🔄 AST Traversal & Target Handler Priority
384
+
385
+ During Prettier AST preprocessing (`walkAst`), each node is checked against target handlers in a prioritized, short-circuiting pipeline:
386
+
387
+ $$\text{JSX Attributes} \longrightarrow \text{CVA Calls} \longrightarrow \text{Object Properties}$$
388
+
389
+ Once a target handler matches and formats a node, traversal stops for that node and moves on to child nodes. This deterministic hierarchy ensures no overlapping or conflicting AST transformations.
390
+
391
+ ---
392
+
393
+ ## 🧩 Class Grouping Strategy
394
+
395
+ When breaking a long `className` string into multiple arguments, the plugin uses a **Category-First + Modifier Threshold Pipeline**:
396
+
397
+ 1. **Category Grouping**: Classes are partitioned into **14 primary categories** (plus 1 Unknown/Custom category for unrecognized or external classes). For the complete ordered category definition and Tailwind CSS utility mappings, see [src/orders/class-groups.ts](src/orders/class-groups.ts).
398
+ 2. **Base vs. Modifiers**: Within each category, classes without modifiers (base classes) and classes with modifiers (`hover:`, `dark:`, `sm:`, etc.) are separated.
399
+ 3. **Threshold Decision (`modifierThreshold`, default `2`)**:
400
+ - **`≤ modifierThreshold` (≤ 2 modifiers)**: Modifiers are kept on the **same line** as the base classes for compactness (e.g. `'text-sm font-medium hover:text-blue-600'`).
401
+ - **`> modifierThreshold` (> 2 modifiers)**: Base classes are placed on the **first line**, followed by each modifier variant cluster on its **own line**, sorted by Tailwind's official variant hierarchy.
402
+
403
+ ## 💡 Examples
404
+
405
+ ### Button - [shadcn/ui](https://ui.shadcn.com/docs/components/base/button)
406
+
407
+ Here is how `@culur/prettier-plugin-tailwindcss-group` automatically formats and groups complex Tailwind utility classes in a `Button` component from Button (using `cva` and `@base-ui/react`):
408
+
409
+ <!-- test-case: shadcn classNameThreshold=5 modifierThreshold=2 -->
410
+
411
+ <details>
412
+ <summary><strong>Before (Single-line unformatted string)</strong></summary>
413
+
414
+ ```tsx
415
+ import { Button as ButtonPrimitive } from '@base-ui/react/button';
416
+ import { cva, type VariantProps } from 'class-variance-authority';
417
+ import { cn } from '@/lib/utils';
418
+
419
+ const buttonVariants = cva(
420
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
421
+ {
422
+ variants: {
423
+ variant: {
424
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
425
+ outline:
426
+ 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
427
+ destructive:
428
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
429
+ },
430
+ size: {
431
+ default:
432
+ 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
433
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
434
+ icon: 'size-8',
435
+ },
436
+ },
437
+ defaultVariants: {
438
+ variant: 'default',
439
+ size: 'default',
440
+ },
441
+ },
442
+ );
443
+
444
+ function Button({
445
+ className,
446
+ variant = 'default',
447
+ size = 'default',
448
+ ...props
449
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
450
+ return (
451
+ <ButtonPrimitive
452
+ data-slot="button"
453
+ className={cn(buttonVariants({ variant, size, className }))}
454
+ {...props}
455
+ />
456
+ );
457
+ }
458
+
459
+ export { Button, buttonVariants };
460
+ ```
461
+
462
+ </details>
463
+
464
+ <details>
465
+ <summary><strong>After (Formatted with Category & Modifier Grouping)</strong></summary>
466
+
467
+ ```tsx
468
+ import { Button as ButtonPrimitive } from '@base-ui/react/button';
469
+ import { cva, type VariantProps } from 'class-variance-authority';
470
+ import { cn } from '@/lib/utils';
471
+
472
+ const buttonVariants = cva(
473
+ [
474
+ 'group/button',
475
+ 'inline-flex shrink-0 items-center justify-center [&_svg]:shrink-0',
476
+ "[&_svg:not([class*='size-'])]:size-4",
477
+ 'text-sm font-medium whitespace-nowrap',
478
+ 'bg-clip-padding',
479
+ 'rounded-lg border border-transparent outline-none',
480
+ 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50',
481
+ 'aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20',
482
+ 'dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
483
+ 'disabled:opacity-50',
484
+ 'transition-all active:not-aria-[haspopup]:translate-y-px',
485
+ 'select-none disabled:pointer-events-none [&_svg]:pointer-events-none',
486
+ ],
487
+ {
488
+ variants: {
489
+ variant: {
490
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
491
+ outline: [
492
+ 'hover:text-foreground aria-expanded:text-foreground',
493
+ 'bg-background',
494
+ 'hover:bg-muted',
495
+ 'aria-expanded:bg-muted',
496
+ 'dark:bg-input/30',
497
+ 'dark:hover:bg-input/50',
498
+ 'border-border dark:border-input',
499
+ ],
500
+ destructive: [
501
+ 'text-destructive',
502
+ 'bg-destructive/10',
503
+ 'hover:bg-destructive/20',
504
+ 'dark:bg-destructive/20',
505
+ 'dark:hover:bg-destructive/30',
506
+ 'focus-visible:border-destructive/40 focus-visible:ring-destructive/20',
507
+ 'dark:focus-visible:ring-destructive/40',
508
+ ],
509
+ },
510
+ size: {
511
+ default:
512
+ 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
513
+ sm: [
514
+ 'gap-1',
515
+ 'h-7 px-2.5',
516
+ 'has-data-[icon=inline-end]:pr-1.5',
517
+ 'has-data-[icon=inline-start]:pl-1.5',
518
+ "[&_svg:not([class*='size-'])]:size-3.5",
519
+ 'text-[0.8rem]',
520
+ 'rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
521
+ ],
522
+ icon: 'size-8',
523
+ },
524
+ },
525
+ defaultVariants: {
526
+ variant: 'default',
527
+ size: 'default',
528
+ },
529
+ },
530
+ );
531
+
532
+ function Button({
533
+ className,
534
+ variant = 'default',
535
+ size = 'default',
536
+ ...props
537
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
538
+ return (
539
+ <ButtonPrimitive
540
+ data-slot="button"
541
+ className={cn(buttonVariants({ variant, size, className }))}
542
+ {...props}
543
+ />
544
+ );
545
+ }
546
+
547
+ export { Button, buttonVariants };
548
+ ```
549
+
550
+ </details>
551
+
552
+ ## 🗃️ Changelog
553
+
554
+ See [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
555
+
556
+ ## 🔒 License
557
+
558
+ See [LICENSE](../../LICENSE) for license rights and limitations (MIT).