@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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +558 -0
- package/dist/index.d.mts +673 -0
- package/dist/index.mjs +4194 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +66 -0
- package/src/__tests__/comments.test.ts +171 -0
- package/src/__tests__/plugin-official.test.ts +32 -0
- package/src/__tests__/readme.test.ts +13 -0
- package/src/__tests__/utils/ast-estree.ts +22 -0
- package/src/__tests__/utils/ast-md.ts +101 -0
- package/src/__tests__/utils/test-format.ts +35 -0
- package/src/ast/ast-array-expression.test.ts +33 -0
- package/src/ast/ast-array-expression.ts +20 -0
- package/src/ast/ast-call-expression.test.ts +17 -0
- package/src/ast/ast-call-expression.ts +5 -0
- package/src/ast/ast-comment.test.ts +45 -0
- package/src/ast/ast-comment.ts +25 -0
- package/src/ast/ast-common.ts +29 -0
- package/src/ast/ast-jsx.test.ts +56 -0
- package/src/ast/ast-jsx.ts +53 -0
- package/src/ast/ast-literal.test.ts +47 -0
- package/src/ast/ast-literal.ts +17 -0
- package/src/ast/ast-property.test.ts +96 -0
- package/src/ast/ast-property.ts +32 -0
- package/src/ast/index.ts +8 -0
- package/src/ast/key.test.ts +21 -0
- package/src/ast/key.ts +4 -0
- package/src/group-classes/group-classes.bucket.ts +61 -0
- package/src/group-classes/group-classes.format.ts +39 -0
- package/src/group-classes/group-classes.test.ts +126 -0
- package/src/group-classes/group-classes.ts +34 -0
- package/src/group-classes/group-classes.types.ts +5 -0
- package/src/handle-node/handle-cva-class-value.test.ts +133 -0
- package/src/handle-node/handle-cva-class-value.ts +47 -0
- package/src/handle-node/handle-expressions.test.ts +140 -0
- package/src/handle-node/handle-expressions.ts +63 -0
- package/src/handle-node/handle-string-literal.test.ts +135 -0
- package/src/handle-node/handle-string-literal.ts +53 -0
- package/src/handle-targets/index.ts +3 -0
- package/src/handle-targets/target-cva-base.test.ts +103 -0
- package/src/handle-targets/target-cva-base.ts +17 -0
- package/src/handle-targets/target-cva-compound-variants.test.ts +239 -0
- package/src/handle-targets/target-cva-compound-variants.ts +45 -0
- package/src/handle-targets/target-cva-variants.test.ts +178 -0
- package/src/handle-targets/target-cva-variants.ts +50 -0
- package/src/handle-targets/target-cva.test.ts +126 -0
- package/src/handle-targets/target-cva.ts +33 -0
- package/src/handle-targets/target-jsx.test.ts +242 -0
- package/src/handle-targets/target-jsx.ts +65 -0
- package/src/handle-targets/target-object-properties.test.ts +140 -0
- package/src/handle-targets/target-object-properties.ts +41 -0
- package/src/index.ts +13 -0
- package/src/options.test.ts +73 -0
- package/src/options.ts +94 -0
- package/src/orders/class-groups.test.ts +74 -0
- package/src/orders/class-groups.ts +178 -0
- package/src/orders/class-order.tailwind-merge.ts +26 -0
- package/src/orders/modifier-order.prettier-plugin-tailwindcss.test.ts +33 -0
- package/src/orders/modifier-order.prettier-plugin-tailwindcss.ts +23 -0
- package/src/prettier.d.ts +21 -0
- package/src/printers.test.ts +58 -0
- package/src/printers.ts +29 -0
- package/src/printers.walk-ast.test.ts +58 -0
- package/src/printers.walk-ast.ts +41 -0
- package/src/utils/split-classes.ts +3 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { classGroups, classGroupsCount } from './class-groups';
|
|
3
|
+
import { config } from './class-order.tailwind-merge';
|
|
4
|
+
|
|
5
|
+
describe('classGroups validation', () => {
|
|
6
|
+
const tailwindGroupIds = Object.keys(config.classGroups);
|
|
7
|
+
|
|
8
|
+
it('should match classGroupsCount with classGroups length + 1', () => {
|
|
9
|
+
expect(classGroupsCount).toBe(classGroups.length + 1);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should not contain any duplicate groupIds across class groups', () => {
|
|
13
|
+
const seen = new Set<string>();
|
|
14
|
+
const duplicates: string[] = [];
|
|
15
|
+
|
|
16
|
+
for (const groupList of classGroups) {
|
|
17
|
+
for (const id of groupList) {
|
|
18
|
+
if (seen.has(id)) {
|
|
19
|
+
duplicates.push(id);
|
|
20
|
+
}
|
|
21
|
+
seen.add(id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
expect(
|
|
26
|
+
duplicates,
|
|
27
|
+
`Found duplicate groupIds: ${duplicates.join(', ')}`,
|
|
28
|
+
).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should not contain any extraneous groupIds not present in tailwind-merge', () => {
|
|
32
|
+
const validGroupIdsSet = new Set(tailwindGroupIds);
|
|
33
|
+
const extraGroupIds: string[] = [];
|
|
34
|
+
|
|
35
|
+
for (const groupList of classGroups) {
|
|
36
|
+
for (const id of groupList) {
|
|
37
|
+
if (!validGroupIdsSet.has(id)) {
|
|
38
|
+
extraGroupIds.push(id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(
|
|
44
|
+
extraGroupIds,
|
|
45
|
+
`Found extraneous groupIds not in tailwind-merge: ${extraGroupIds.join(', ')}`,
|
|
46
|
+
).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should not omit any groupIds defined in tailwind-merge', () => {
|
|
50
|
+
const declaredGroupIdsSet = new Set<string>();
|
|
51
|
+
for (const groupList of classGroups) {
|
|
52
|
+
for (const id of groupList) {
|
|
53
|
+
declaredGroupIdsSet.add(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const missingGroupIds = tailwindGroupIds.filter(
|
|
58
|
+
id => !declaredGroupIdsSet.has(id),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(
|
|
62
|
+
missingGroupIds,
|
|
63
|
+
`Missing the following groupIds from tailwind-merge: ${missingGroupIds.join(', ')}`,
|
|
64
|
+
).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should have the exact total declared count equal to tailwind-merge classGroups count', () => {
|
|
68
|
+
const totalDeclared = classGroups.reduce(
|
|
69
|
+
(sum, groupList) => sum + groupList.length,
|
|
70
|
+
0,
|
|
71
|
+
);
|
|
72
|
+
expect(totalDeclared).toBe(tailwindGroupIds.length);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groups Tailwind CSS classGroupIds into 14 primary categories (+ 1 unknown group) for the plugin.
|
|
3
|
+
* Each category contains a list of corresponding groupIds from tailwind-merge.
|
|
4
|
+
*/
|
|
5
|
+
export const classGroups: readonly (readonly string[])[] = [
|
|
6
|
+
//! Context & Container
|
|
7
|
+
// prettier-multiline-arrays-next-line-pattern: 4
|
|
8
|
+
[
|
|
9
|
+
'group', 'peer', 'container-type', 'container-named',
|
|
10
|
+
],
|
|
11
|
+
|
|
12
|
+
//! Positioning
|
|
13
|
+
// prettier-multiline-arrays-next-line-pattern: 1 7 4 3
|
|
14
|
+
[
|
|
15
|
+
'position', // position
|
|
16
|
+
'inset', 'inset-x', 'inset-y', 'start', 'end', 'inset-bs', 'inset-be', // inset
|
|
17
|
+
'top', 'right', 'bottom', 'left', // top, right, bottom, left
|
|
18
|
+
'float', 'clear', 'z', // float, clear, z-index
|
|
19
|
+
],
|
|
20
|
+
|
|
21
|
+
//! Display, Flex, Grid
|
|
22
|
+
// prettier-multiline-arrays-next-line-pattern: 4 7 4 4 3 3 3 3 3 1
|
|
23
|
+
[
|
|
24
|
+
'display', 'sr', 'isolation', 'visibility', // display & visibility
|
|
25
|
+
'flex-direction', 'flex-wrap', 'flex', 'grow', 'shrink', 'basis', 'order', // flex
|
|
26
|
+
'grid-cols', 'col-start-end', 'col-start', 'col-end', // grid column
|
|
27
|
+
'grid-rows', 'row-start-end', 'row-start', 'row-end', // grid row
|
|
28
|
+
'grid-flow', 'auto-cols', 'auto-rows', // grid flow
|
|
29
|
+
'gap', 'gap-x', 'gap-y', // gap
|
|
30
|
+
'justify-content', 'justify-items', 'justify-self', // justify
|
|
31
|
+
'align-content', 'align-items', 'align-self', // align
|
|
32
|
+
'place-content', 'place-items', 'place-self', // place
|
|
33
|
+
'columns', // columns
|
|
34
|
+
],
|
|
35
|
+
|
|
36
|
+
//! Box Model & Spacing
|
|
37
|
+
// prettier-multiline-arrays-next-line-pattern: 8 3 3 1 1 3 11 11 4
|
|
38
|
+
[
|
|
39
|
+
'w', 'min-w', 'max-w', 'h', 'min-h', 'max-h', 'size', 'aspect', // size
|
|
40
|
+
'inline-size', 'min-inline-size', 'max-inline-size', // inline-size
|
|
41
|
+
'block-size', 'min-block-size', 'max-block-size', // block-size
|
|
42
|
+
'box', // box-sizing
|
|
43
|
+
'container', // container
|
|
44
|
+
'break-after', 'break-before', 'break-inside', // break
|
|
45
|
+
'p', 'px', 'py', 'ps', 'pe', 'pbs', 'pbe', 'pt', 'pr', 'pb', 'pl', // padding
|
|
46
|
+
'm', 'mx', 'my', 'ms', 'me', 'mbs', 'mbe', 'mt', 'mr', 'mb', 'ml', // margin
|
|
47
|
+
'space-x', 'space-x-reverse', 'space-y', 'space-y-reverse', // space
|
|
48
|
+
],
|
|
49
|
+
|
|
50
|
+
//! Overflow & Scroll
|
|
51
|
+
// prettier-multiline-arrays-next-line-pattern: 6 1 4 11 11
|
|
52
|
+
[
|
|
53
|
+
'overflow', 'overflow-x', 'overflow-y', 'overscroll', 'overscroll-x', 'overscroll-y', // overflow
|
|
54
|
+
'scroll-behavior', // scroll-behavior
|
|
55
|
+
'scrollbar-thumb-color', 'scrollbar-track-color', 'scrollbar-gutter', 'scrollbar-w', // scrollbar
|
|
56
|
+
'scroll-m', 'scroll-mx', 'scroll-my', 'scroll-ms', 'scroll-me', 'scroll-mbs', 'scroll-mbe', 'scroll-mt', 'scroll-mr', 'scroll-mb', 'scroll-ml', // scroll margin
|
|
57
|
+
'scroll-p', 'scroll-px', 'scroll-py', 'scroll-ps', 'scroll-pe', 'scroll-pbs', 'scroll-pbe', 'scroll-pt', 'scroll-pr', 'scroll-pb', 'scroll-pl', // scroll padding
|
|
58
|
+
],
|
|
59
|
+
|
|
60
|
+
//! Typography
|
|
61
|
+
// prettier-multiline-arrays-next-line-pattern: 7 6 3 2 2 5 7 3 2 2 1
|
|
62
|
+
[
|
|
63
|
+
'font-size', 'font-smoothing', 'font-style', 'font-weight', 'font-stretch', 'font-family', 'font-features', // font
|
|
64
|
+
'fvn-normal', 'fvn-ordinal', 'fvn-slashed-zero', 'fvn-figure', 'fvn-spacing', 'fvn-fraction', // font-variant-numeric
|
|
65
|
+
'list-image', 'list-style-position', 'list-style-type', // list
|
|
66
|
+
'placeholder-color', 'text-color', // color
|
|
67
|
+
'text-shadow', 'text-shadow-color', // text-shadow
|
|
68
|
+
'text-decoration', 'text-decoration-style', 'text-decoration-thickness', 'text-decoration-color', 'underline-offset', // underline
|
|
69
|
+
'line-clamp', 'text-overflow', 'text-wrap', 'whitespace', 'break', 'wrap', 'hyphens', // text wrap
|
|
70
|
+
'tracking', 'leading', 'text-transform', // transform
|
|
71
|
+
'indent', 'tab-size', // indent
|
|
72
|
+
'vertical-align', 'text-alignment', // align
|
|
73
|
+
'content', // content
|
|
74
|
+
],
|
|
75
|
+
|
|
76
|
+
//! Background
|
|
77
|
+
// prettier-multiline-arrays-next-line-pattern: 8 6 1 2
|
|
78
|
+
[
|
|
79
|
+
'bg-attachment', 'bg-clip', 'bg-origin', 'bg-position', 'bg-repeat', 'bg-size', 'bg-image', 'bg-color', // background
|
|
80
|
+
'gradient-from-pos', 'gradient-via-pos', 'gradient-to-pos', 'gradient-from', 'gradient-via', 'gradient-to', // gradient
|
|
81
|
+
'box-decoration', // box-decoration
|
|
82
|
+
'object-fit', 'object-position', // object (image)
|
|
83
|
+
],
|
|
84
|
+
|
|
85
|
+
//! Filter
|
|
86
|
+
// prettier-multiline-arrays-next-line-pattern: 11 10
|
|
87
|
+
[
|
|
88
|
+
'filter', 'blur', 'brightness', 'contrast', 'drop-shadow', 'drop-shadow-color', 'grayscale', 'hue-rotate', 'invert', 'saturate', 'sepia', // filter
|
|
89
|
+
'backdrop-filter', 'backdrop-blur', 'backdrop-brightness', 'backdrop-contrast', 'backdrop-grayscale', 'backdrop-hue-rotate', 'backdrop-invert', 'backdrop-opacity', 'backdrop-saturate', 'backdrop-sepia', // backdrop-filter
|
|
90
|
+
],
|
|
91
|
+
|
|
92
|
+
//! Border & Shadow
|
|
93
|
+
// prettier-multiline-arrays-next-line-pattern: 3 4 4 1 3 4 4 4 2 3 4 4 4 4 2 2 5 2 2
|
|
94
|
+
[
|
|
95
|
+
'border-w', 'border-w-x', 'border-w-y', // border width
|
|
96
|
+
'border-w-s', 'border-w-e', 'border-w-bs', 'border-w-be',
|
|
97
|
+
'border-w-t', 'border-w-r', 'border-w-b', 'border-w-l',
|
|
98
|
+
|
|
99
|
+
'border-style',
|
|
100
|
+
'border-color', 'border-color-x', 'border-color-y', // border color
|
|
101
|
+
'border-color-s', 'border-color-e', 'border-color-bs', 'border-color-be',
|
|
102
|
+
'border-color-t', 'border-color-r', 'border-color-b', 'border-color-l',
|
|
103
|
+
|
|
104
|
+
'divide-x', 'divide-x-reverse', 'divide-y', 'divide-y-reverse', // divide
|
|
105
|
+
'divide-style', 'divide-color', // divide style & divide color
|
|
106
|
+
|
|
107
|
+
'rounded', 'rounded-s', 'rounded-e', // rounded
|
|
108
|
+
'rounded-t', 'rounded-r', 'rounded-b', 'rounded-l',
|
|
109
|
+
'rounded-ss', 'rounded-se', 'rounded-ee', 'rounded-es',
|
|
110
|
+
'rounded-tl', 'rounded-tr', 'rounded-br', 'rounded-bl',
|
|
111
|
+
|
|
112
|
+
'outline-style', 'outline-offset', 'outline-w', 'outline-color', // outline
|
|
113
|
+
|
|
114
|
+
'shadow', 'shadow-color', // shadow
|
|
115
|
+
'inset-shadow', 'inset-shadow-color', // inset-shadow
|
|
116
|
+
'ring-w', 'ring-w-inset', 'ring-color', 'ring-offset-w', 'ring-offset-color', // ring
|
|
117
|
+
'inset-ring-w', 'inset-ring-color', // inset-ring
|
|
118
|
+
],
|
|
119
|
+
|
|
120
|
+
//! Effect & Mask
|
|
121
|
+
// prettier-multiline-arrays-next-line-pattern: 3 2 5 4 4 4 4 4 4 5 3 5 7
|
|
122
|
+
[
|
|
123
|
+
'opacity', 'mix-blend', 'bg-blend',
|
|
124
|
+
'mask-clip', 'mask-composite',
|
|
125
|
+
'mask-image-linear-pos', 'mask-image-linear-from-pos', 'mask-image-linear-to-pos', 'mask-image-linear-from-color', 'mask-image-linear-to-color', // mask-image-linear
|
|
126
|
+
'mask-image-t-from-pos', 'mask-image-t-to-pos', 'mask-image-t-from-color', 'mask-image-t-to-color', // mask-image-t
|
|
127
|
+
'mask-image-r-from-pos', 'mask-image-r-to-pos', 'mask-image-r-from-color', 'mask-image-r-to-color', // mask-image-r
|
|
128
|
+
'mask-image-b-from-pos', 'mask-image-b-to-pos', 'mask-image-b-from-color', 'mask-image-b-to-color', // mask-image-b
|
|
129
|
+
'mask-image-l-from-pos', 'mask-image-l-to-pos', 'mask-image-l-from-color', 'mask-image-l-to-color', // mask-image-l
|
|
130
|
+
'mask-image-x-from-pos', 'mask-image-x-to-pos', 'mask-image-x-from-color', 'mask-image-x-to-color', // mask-image-x
|
|
131
|
+
'mask-image-y-from-pos', 'mask-image-y-to-pos', 'mask-image-y-from-color', 'mask-image-y-to-color', // mask-image-y
|
|
132
|
+
|
|
133
|
+
'mask-image-radial', 'mask-image-radial-from-pos', 'mask-image-radial-to-pos', 'mask-image-radial-from-color', 'mask-image-radial-to-color', // mask-image-radial
|
|
134
|
+
'mask-image-radial-shape', 'mask-image-radial-size', 'mask-image-radial-pos', // mask-image-radial
|
|
135
|
+
'mask-image-conic-pos', 'mask-image-conic-from-pos', 'mask-image-conic-to-pos', 'mask-image-conic-from-color', 'mask-image-conic-to-color', // mask-image-conic
|
|
136
|
+
'mask-mode', 'mask-origin', 'mask-position', 'mask-repeat', 'mask-size', 'mask-type', 'mask-image', // mask others
|
|
137
|
+
],
|
|
138
|
+
|
|
139
|
+
//! Table
|
|
140
|
+
// prettier-multiline-arrays-next-line-pattern: 6
|
|
141
|
+
[
|
|
142
|
+
'border-collapse', 'border-spacing', 'border-spacing-x', 'border-spacing-y', 'table-layout', 'caption',
|
|
143
|
+
],
|
|
144
|
+
|
|
145
|
+
//! Transition & Transform
|
|
146
|
+
// prettier-multiline-arrays-next-line-pattern: 5 4 4 5 3 8 1
|
|
147
|
+
[
|
|
148
|
+
'transition', 'transition-behavior', 'duration', 'ease', 'delay', // transition & animation
|
|
149
|
+
'animate', 'backface', 'perspective', 'perspective-origin', // transforms
|
|
150
|
+
'rotate', 'rotate-x', 'rotate-y', 'rotate-z', // rotate
|
|
151
|
+
'scale', 'scale-x', 'scale-y', 'scale-z', 'scale-3d', // scale
|
|
152
|
+
'skew', 'skew-x', 'skew-y', // skew
|
|
153
|
+
'transform', 'transform-origin', 'transform-style', 'translate', 'translate-x', 'translate-y', 'translate-z', 'translate-none', // transform
|
|
154
|
+
'zoom', // zoom
|
|
155
|
+
],
|
|
156
|
+
|
|
157
|
+
//! Interactivity
|
|
158
|
+
// prettier-multiline-arrays-next-line-pattern: 4 3 2 4 4 2
|
|
159
|
+
[
|
|
160
|
+
'accent', 'forced-color-adjust', 'appearance', 'color-scheme',
|
|
161
|
+
'caret-color', 'cursor', 'pointer-events', // caret & cursor
|
|
162
|
+
'field-sizing', 'resize', // resize
|
|
163
|
+
'snap-align', 'snap-stop', 'snap-type', 'snap-strictness', // snap
|
|
164
|
+
'touch', 'touch-x', 'touch-y', 'touch-pz', // touch
|
|
165
|
+
'select', 'will-change', //
|
|
166
|
+
],
|
|
167
|
+
|
|
168
|
+
//! SVG
|
|
169
|
+
// prettier-multiline-arrays-next-line-pattern: 3
|
|
170
|
+
[
|
|
171
|
+
'fill', 'stroke-w', 'stroke',
|
|
172
|
+
],
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Total class group count (14 standard groups + 1 Unknown/Custom group at the end).
|
|
177
|
+
*/
|
|
178
|
+
export const classGroupsCount = classGroups.length + 1;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ConfigExtension } from 'tailwind-merge-ts';
|
|
2
|
+
import type { AnyThemeGroupIds } from 'tailwind-merge-ts/lib/types';
|
|
3
|
+
import { getDefaultConfig, mergeConfigs } from 'tailwind-merge-ts';
|
|
4
|
+
import { createConfigUtils } from 'tailwind-merge-ts/lib/config-utils';
|
|
5
|
+
|
|
6
|
+
type ExtendedGroups = 'group' | 'peer';
|
|
7
|
+
|
|
8
|
+
const isGroupNamed = (value: string) => value.startsWith('group/');
|
|
9
|
+
const isPeerNamed = (value: string) => value.startsWith('peer/');
|
|
10
|
+
|
|
11
|
+
const configExtend: ConfigExtension<
|
|
12
|
+
ExtendedGroups,
|
|
13
|
+
AnyThemeGroupIds
|
|
14
|
+
>['extend'] = {
|
|
15
|
+
classGroups: {
|
|
16
|
+
group: ['group', isGroupNamed],
|
|
17
|
+
peer: ['peer', isPeerNamed],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Initialize singleton config utils once at module evaluation
|
|
22
|
+
export const config = mergeConfigs<ExtendedGroups>(getDefaultConfig(), {
|
|
23
|
+
extend: configExtend,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const configUtils = createConfigUtils(config);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { sortModifierKeys } from './modifier-order.prettier-plugin-tailwindcss';
|
|
3
|
+
|
|
4
|
+
describe('sortModifierKeys with Tailwind CSS Compiler sorter', () => {
|
|
5
|
+
it('returns array as is when length <= 1', () => {
|
|
6
|
+
expect(sortModifierKeys([])).toEqual([]);
|
|
7
|
+
expect(sortModifierKeys(['hover'])).toEqual(['hover']);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('correctly sorts standard Tailwind CSS modifiers', () => {
|
|
11
|
+
const input = ['dark:hover', 'hover', 'md', 'dark', 'sm'];
|
|
12
|
+
const sorted = sortModifierKeys(input);
|
|
13
|
+
|
|
14
|
+
expect(sorted).toEqual(['hover', 'sm', 'md', 'dark', 'dark:hover']);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('handles arbitrary variants and bracket syntax without corruption', () => {
|
|
18
|
+
const input = [
|
|
19
|
+
'hover',
|
|
20
|
+
'[@supports(display:grid)]:hover',
|
|
21
|
+
'dark',
|
|
22
|
+
'[&_svg]:hover',
|
|
23
|
+
];
|
|
24
|
+
const sorted = sortModifierKeys(input);
|
|
25
|
+
|
|
26
|
+
expect(sorted).toEqual([
|
|
27
|
+
'hover',
|
|
28
|
+
'dark',
|
|
29
|
+
'[&_svg]:hover',
|
|
30
|
+
'[@supports(display:grid)]:hover',
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createSorter } from 'prettier-plugin-tailwindcss/sorter';
|
|
2
|
+
|
|
3
|
+
// Initialize Tailwind Compiler sorter once at module evaluation
|
|
4
|
+
const sorter = await createSorter({});
|
|
5
|
+
const DUMMY_SUFFIX = ':flex';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sorts modifier keys according to Tailwind CSS Compiler AST order
|
|
9
|
+
* using dummy classes (e.g. 'hover' -> 'hover:flex').
|
|
10
|
+
*
|
|
11
|
+
* @param modifierKeys Array of modifier strings (e.g. ['dark:hover', 'hover', 'md', 'dark', 'sm'])
|
|
12
|
+
* @returns Sorted array of modifier keys adhering to official Tailwind Compiler order
|
|
13
|
+
*/
|
|
14
|
+
export function sortModifierKeys(modifierKeys: string[]): string[] {
|
|
15
|
+
if (modifierKeys.length <= 1) {
|
|
16
|
+
return modifierKeys;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const dummyClasses = modifierKeys.map(mod => `${mod}${DUMMY_SUFFIX}`);
|
|
20
|
+
const [sortedDummyClasses] = sorter.sortClassLists([dummyClasses]);
|
|
21
|
+
|
|
22
|
+
return sortedDummyClasses.map(cls => cls.slice(0, -DUMMY_SUFFIX.length));
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Options } from 'prettier';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ambient type augmentation for Prettier Options.
|
|
5
|
+
*
|
|
6
|
+
* NOTE: These type definitions correspond directly to the runtime options defined in:
|
|
7
|
+
* @see {@link ./options.ts}
|
|
8
|
+
*/
|
|
9
|
+
declare module 'prettier' {
|
|
10
|
+
interface Options {
|
|
11
|
+
classNameThreshold?: number;
|
|
12
|
+
modifierThreshold?: number;
|
|
13
|
+
tailwindFunctions?: string[];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PluginOptions extends Options {
|
|
18
|
+
classNameThreshold: number;
|
|
19
|
+
modifierThreshold: number;
|
|
20
|
+
tailwindFunctions: string[];
|
|
21
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Node } from 'estree-jsx';
|
|
2
|
+
import type { ParserOptions } from 'prettier';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { afterEach, beforeEach, describe } from 'vitest';
|
|
5
|
+
import { testFormat } from './__tests__/utils/test-format';
|
|
6
|
+
import { estree } from './ast';
|
|
7
|
+
|
|
8
|
+
describe('printers preprocess handling: async Promise AST preprocess', () => {
|
|
9
|
+
let originalPreprocess:
|
|
10
|
+
| undefined
|
|
11
|
+
| ((ast: Node, _options: ParserOptions<Node>) => Node | Promise<Node>);
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
originalPreprocess = estree.preprocess;
|
|
15
|
+
estree.preprocess = (ast, _options) => Promise.resolve(ast);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
estree.preprocess = originalPreprocess;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
testFormat(
|
|
23
|
+
'correctly handles async Promise returned by estree.preprocess',
|
|
24
|
+
{ classNameThreshold: 2 },
|
|
25
|
+
dedent /* tsx */ `
|
|
26
|
+
<div className="flex items-center text-sm font-bold" />;
|
|
27
|
+
`,
|
|
28
|
+
dedent /* tsx */ `
|
|
29
|
+
<div className={cn('flex items-center', 'text-sm font-bold')} />;
|
|
30
|
+
`,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('printers preprocess handling: fallback when estree.preprocess is undefined (sync path)', () => {
|
|
35
|
+
let originalPreprocess:
|
|
36
|
+
| undefined
|
|
37
|
+
| ((ast: Node, _options: ParserOptions<Node>) => Node | Promise<Node>);
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
originalPreprocess = estree.preprocess;
|
|
41
|
+
estree.preprocess = undefined;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
estree.preprocess = originalPreprocess;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
testFormat(
|
|
49
|
+
'uses ast directly if estree.preprocess is undefined',
|
|
50
|
+
{ classNameThreshold: 2 },
|
|
51
|
+
dedent /* tsx */ `
|
|
52
|
+
<div className="flex items-center text-sm font-bold" />;
|
|
53
|
+
`,
|
|
54
|
+
dedent /* tsx */ `
|
|
55
|
+
<div className={cn('flex items-center', 'text-sm font-bold')} />;
|
|
56
|
+
`,
|
|
57
|
+
);
|
|
58
|
+
});
|
package/src/printers.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Node } from 'estree-jsx';
|
|
2
|
+
import type { Plugin } from 'prettier';
|
|
3
|
+
import estreePlugin from 'prettier/plugins/estree.js';
|
|
4
|
+
import { estree } from './ast';
|
|
5
|
+
import { resolveOptions } from './options';
|
|
6
|
+
import { walkAst } from './printers.walk-ast';
|
|
7
|
+
|
|
8
|
+
export const printers: Plugin<Node>['printers'] = {
|
|
9
|
+
estree: {
|
|
10
|
+
...estreePlugin.printers.estree,
|
|
11
|
+
preprocess(ast, options) {
|
|
12
|
+
const pluginOptions = resolveOptions(options);
|
|
13
|
+
|
|
14
|
+
const processedAST = estree.preprocess
|
|
15
|
+
? estree.preprocess(ast, options)
|
|
16
|
+
: ast;
|
|
17
|
+
|
|
18
|
+
if (processedAST instanceof Promise) {
|
|
19
|
+
return processedAST.then(resolvedAst => {
|
|
20
|
+
walkAst(resolvedAst, pluginOptions);
|
|
21
|
+
return resolvedAst;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
walkAst(processedAST, pluginOptions);
|
|
26
|
+
return processedAST;
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Node } from 'estree-jsx';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { estree } from './ast';
|
|
4
|
+
import { resolveOptions } from './options';
|
|
5
|
+
import { walkAst } from './printers.walk-ast';
|
|
6
|
+
|
|
7
|
+
describe('walkAst unit tests', () => {
|
|
8
|
+
const options = resolveOptions({});
|
|
9
|
+
|
|
10
|
+
it('returns early when input is not a valid AST Node', () => {
|
|
11
|
+
expect(() => walkAst(null as unknown as Node, options)).not.toThrow();
|
|
12
|
+
expect(() => walkAst(undefined as unknown as Node, options)).not.toThrow();
|
|
13
|
+
expect(() => walkAst(123 as unknown as Node, options)).not.toThrow();
|
|
14
|
+
expect(() =>
|
|
15
|
+
walkAst('not-a-node' as unknown as Node, options),
|
|
16
|
+
).not.toThrow();
|
|
17
|
+
expect(() => walkAst({} as unknown as Node, options)).not.toThrow();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('handles array child properties containing non-node items', () => {
|
|
21
|
+
const mockNode: any = {
|
|
22
|
+
type: 'Program',
|
|
23
|
+
body: [null, undefined, 123, 'string-item', { type: 'EmptyStatement' }],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
expect(() => walkAst(mockNode, options)).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('handles non-array child properties that are nodes', () => {
|
|
30
|
+
const mockNode: any = {
|
|
31
|
+
type: 'ExpressionStatement',
|
|
32
|
+
expression: {
|
|
33
|
+
type: 'Identifier',
|
|
34
|
+
name: 'foo',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
expect(() => walkAst(mockNode, options)).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('handles truthy non-array child properties that are not nodes', () => {
|
|
42
|
+
const originalGetVisitorKeys = estree.getVisitorKeys;
|
|
43
|
+
vi.spyOn(estree, 'getVisitorKeys').mockReturnValue(['someProp'] as any);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const mockNode: any = {
|
|
47
|
+
type: 'CustomNode',
|
|
48
|
+
someProp: 'truthy-non-node-primitive',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
expect(() => walkAst(mockNode, options)).not.toThrow();
|
|
52
|
+
} finally {
|
|
53
|
+
if (originalGetVisitorKeys) {
|
|
54
|
+
estree.getVisitorKeys = originalGetVisitorKeys;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Node } from 'estree-jsx';
|
|
2
|
+
import type { PluginOptions } from './prettier';
|
|
3
|
+
import { estree, isNode } from './ast';
|
|
4
|
+
import {
|
|
5
|
+
handleTargetCVA,
|
|
6
|
+
handleTargetJSX,
|
|
7
|
+
handleTargetObjectProperty,
|
|
8
|
+
} from './handle-targets';
|
|
9
|
+
|
|
10
|
+
const NON_TRAVERSABLE_KEYS = new Set<string>(['parent', 'comments']);
|
|
11
|
+
|
|
12
|
+
const TARGET_HANDLERS = [
|
|
13
|
+
handleTargetJSX,
|
|
14
|
+
handleTargetCVA,
|
|
15
|
+
handleTargetObjectProperty,
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export function walkAst(node: Node, options: PluginOptions): void {
|
|
19
|
+
if (!isNode(node)) return;
|
|
20
|
+
|
|
21
|
+
for (const handle of TARGET_HANDLERS) {
|
|
22
|
+
if (handle(node, options)) break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const visitorKeys = estree.getVisitorKeys!(node, NON_TRAVERSABLE_KEYS);
|
|
26
|
+
|
|
27
|
+
for (const key of visitorKeys) {
|
|
28
|
+
const child = node[key as Exclude<keyof typeof node, 'type'>];
|
|
29
|
+
if (!child) continue;
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(child)) {
|
|
32
|
+
for (const item of child) {
|
|
33
|
+
if (isNode(item)) {
|
|
34
|
+
walkAst(item, options);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} else if (isNode(child)) {
|
|
38
|
+
walkAst(child, options);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|