@runtimestudio/tailwind-sort-php 0.2.1 → 0.3.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/src/islands.ts CHANGED
@@ -14,25 +14,25 @@
14
14
  * A contiguous region of PHP code within a mixed template source.
15
15
  */
16
16
  export interface Island {
17
- /**
18
- * Inclusive start offset of `<?`.
19
- */
20
- start: number;
21
- /**
22
- * Exclusive end offset (just past `?>`, or EOF).
23
- */
24
- end: number;
17
+ /**
18
+ * Inclusive start offset of `<?`.
19
+ */
20
+ start: number;
21
+ /**
22
+ * Exclusive end offset (just past `?>`, or EOF).
23
+ */
24
+ end: number;
25
25
  }
26
26
 
27
27
  /**
28
28
  * Options controlling PHP open-tag recognition.
29
29
  */
30
30
  export interface IslandOptions {
31
- /**
32
- * Treat bare `<?` as a PHP open tag (short_open_tag).
33
- * Default true, with a guard so `<?xml` is never treated as PHP.
34
- */
35
- shortOpenTags?: boolean;
31
+ /**
32
+ * Treat bare `<?` as a PHP open tag (short_open_tag).
33
+ * Default true, with a guard so `<?xml` is never treated as PHP.
34
+ */
35
+ shortOpenTags?: boolean;
36
36
  }
37
37
 
38
38
  const isIdentStart = (c: string) => /[A-Za-z_\u0080-\uffff]/.test(c);
@@ -53,35 +53,35 @@ const isIdent = (c: string) => /[A-Za-z0-9_\u0080-\uffff]/.test(c);
53
53
  * // [{ start: 3, end: 12 }]
54
54
  */
55
55
  export function findIslands(src: string, opts: IslandOptions = {}): Island[] {
56
- const shortTags = opts.shortOpenTags !== false;
57
- const islands: Island[] = [];
58
- const len = src.length;
59
- let i = 0;
60
-
61
- while (i < len) {
62
- const open = src.indexOf('<?', i);
63
- if (open === -1) break;
64
-
65
- // Classify the open tag.
66
- const after = src.slice(open + 2, open + 6).toLowerCase();
67
- let bodyStart: number;
68
- if (after.startsWith('php') && (open + 5 >= len || !isIdent(src[open + 5]))) {
69
- bodyStart = open + 5;
70
- } else if (src[open + 2] === '=') {
71
- bodyStart = open + 3;
72
- } else if (shortTags && !after.startsWith('xml')) {
73
- bodyStart = open + 2;
74
- } else {
75
- i = open + 2; // not a PHP tag (e.g. `<?xml`) — keep scanning
76
- continue;
56
+ const shortTags = opts.shortOpenTags !== false;
57
+ const islands: Island[] = [];
58
+ const len = src.length;
59
+ let i = 0;
60
+
61
+ while (i < len) {
62
+ const open = src.indexOf('<?', i);
63
+ if (open === -1) break;
64
+
65
+ // Classify the open tag.
66
+ const after = src.slice(open + 2, open + 6).toLowerCase();
67
+ let bodyStart: number;
68
+ if (after.startsWith('php') && (open + 5 >= len || !isIdent(src[open + 5]))) {
69
+ bodyStart = open + 5;
70
+ } else if (src[open + 2] === '=') {
71
+ bodyStart = open + 3;
72
+ } else if (shortTags && !after.startsWith('xml')) {
73
+ bodyStart = open + 2;
74
+ } else {
75
+ i = open + 2; // not a PHP tag (e.g. `<?xml`) — keep scanning
76
+ continue;
77
+ }
78
+
79
+ const end = scanPhpBody(src, bodyStart);
80
+ islands.push({ start: open, end });
81
+ i = end;
77
82
  }
78
83
 
79
- const end = scanPhpBody(src, bodyStart);
80
- islands.push({ start: open, end });
81
- i = end;
82
- }
83
-
84
- return islands;
84
+ return islands;
85
85
  }
86
86
 
87
87
  /**
@@ -89,68 +89,68 @@ export function findIslands(src: string, opts: IslandOptions = {}): Island[] {
89
89
  * or `src.length` if the file ends in PHP mode.
90
90
  */
91
91
  function scanPhpBody(src: string, i: number): number {
92
- const len = src.length;
93
-
94
- while (i < len) {
95
- const c = src[i];
96
-
97
- // Possible close tag.
98
- if (c === '?' && src[i + 1] === '>') return i + 2;
99
-
100
- // Single-quoted string.
101
- if (c === "'") {
102
- i = scanQuoted(src, i + 1, "'");
103
- continue;
104
- }
105
-
106
- // Double-quoted string.
107
- // Limitation: nested double quotes in complex `{$a["k"]}` interpolation can desync the lexer; see README.
108
- if (c === '"') {
109
- i = scanQuoted(src, i + 1, '"');
110
- continue;
111
- }
112
-
113
- // Backtick (shell exec) string — same escaping rules.
114
- if (c === '`') {
115
- i = scanQuoted(src, i + 1, '`');
116
- continue;
117
- }
118
-
119
- // Comments.
120
- if (c === '/' && src[i + 1] === '/') {
121
- i = scanLineComment(src, i + 2);
122
- if (src.startsWith('?>', i)) return i + 2;
123
- continue;
124
- }
125
- if (c === '#') {
126
- // PHP 8 attribute `#[...]` is not a comment.
127
- if (src[i + 1] === '[') {
128
- i += 2;
129
- continue;
130
- }
131
- i = scanLineComment(src, i + 1);
132
- if (src.startsWith('?>', i)) return i + 2;
133
- continue;
134
- }
135
- if (c === '/' && src[i + 1] === '*') {
136
- const close = src.indexOf('*/', i + 2);
137
- i = close === -1 ? len : close + 2;
138
- continue;
92
+ const len = src.length;
93
+
94
+ while (i < len) {
95
+ const c = src[i];
96
+
97
+ // Possible close tag.
98
+ if (c === '?' && src[i + 1] === '>') return i + 2;
99
+
100
+ // Single-quoted string.
101
+ if (c === "'") {
102
+ i = scanQuoted(src, i + 1, "'");
103
+ continue;
104
+ }
105
+
106
+ // Double-quoted string.
107
+ // Limitation: nested double quotes in complex `{$a["k"]}` interpolation can desync the lexer; see README.
108
+ if (c === '"') {
109
+ i = scanQuoted(src, i + 1, '"');
110
+ continue;
111
+ }
112
+
113
+ // Backtick (shell exec) string — same escaping rules.
114
+ if (c === '`') {
115
+ i = scanQuoted(src, i + 1, '`');
116
+ continue;
117
+ }
118
+
119
+ // Comments.
120
+ if (c === '/' && src[i + 1] === '/') {
121
+ i = scanLineComment(src, i + 2);
122
+ if (src.startsWith('?>', i)) return i + 2;
123
+ continue;
124
+ }
125
+ if (c === '#') {
126
+ // PHP 8 attribute `#[...]` is not a comment.
127
+ if (src[i + 1] === '[') {
128
+ i += 2;
129
+ continue;
130
+ }
131
+ i = scanLineComment(src, i + 1);
132
+ if (src.startsWith('?>', i)) return i + 2;
133
+ continue;
134
+ }
135
+ if (c === '/' && src[i + 1] === '*') {
136
+ const close = src.indexOf('*/', i + 2);
137
+ i = close === -1 ? len : close + 2;
138
+ continue;
139
+ }
140
+
141
+ // Heredoc / nowdoc.
142
+ if (c === '<' && src[i + 1] === '<' && src[i + 2] === '<') {
143
+ const here = scanHeredoc(src, i + 3);
144
+ if (here !== -1) {
145
+ i = here;
146
+ continue;
147
+ }
148
+ }
149
+
150
+ i++;
139
151
  }
140
152
 
141
- // Heredoc / nowdoc.
142
- if (c === '<' && src[i + 1] === '<' && src[i + 2] === '<') {
143
- const here = scanHeredoc(src, i + 3);
144
- if (here !== -1) {
145
- i = here;
146
- continue;
147
- }
148
- }
149
-
150
- i++;
151
- }
152
-
153
- return len; // file ends while still in PHP mode
153
+ return len; // file ends while still in PHP mode
154
154
  }
155
155
 
156
156
  /**
@@ -158,17 +158,17 @@ function scanPhpBody(src: string, i: number): number {
158
158
  * Returns offset just past the closing quote (or EOF).
159
159
  */
160
160
  function scanQuoted(src: string, i: number, quote: string): number {
161
- const len = src.length;
162
- while (i < len) {
163
- const c = src[i];
164
- if (c === '\\') {
165
- i += 2;
166
- continue;
161
+ const len = src.length;
162
+ while (i < len) {
163
+ const c = src[i];
164
+ if (c === '\\') {
165
+ i += 2;
166
+ continue;
167
+ }
168
+ if (c === quote) return i + 1;
169
+ i++;
167
170
  }
168
- if (c === quote) return i + 1;
169
- i++;
170
- }
171
- return len;
171
+ return len;
172
172
  }
173
173
 
174
174
  /**
@@ -177,13 +177,13 @@ function scanQuoted(src: string, i: number, quote: string): number {
177
177
  * (left for the caller, since `?>` closes both the comment and the island).
178
178
  */
179
179
  function scanLineComment(src: string, i: number): number {
180
- const len = src.length;
181
- while (i < len) {
182
- if (src[i] === '\n') return i + 1;
183
- if (src[i] === '?' && src[i + 1] === '>') return i;
184
- i++;
185
- }
186
- return len;
180
+ const len = src.length;
181
+ while (i < len) {
182
+ if (src[i] === '\n') return i + 1;
183
+ if (src[i] === '?' && src[i + 1] === '>') return i;
184
+ i++;
185
+ }
186
+ return len;
187
187
  }
188
188
 
189
189
  /**
@@ -191,46 +191,46 @@ function scanLineComment(src: string, i: number): number {
191
191
  * Returns the offset past the closing identifier line, or -1 if `<<<` isn't followed by a valid heredoc identifier.
192
192
  */
193
193
  function scanHeredoc(src: string, i: number): number {
194
- const len = src.length;
195
- // Optional whitespace (PHP allows spaces/tabs after `<<<`).
196
- while (i < len && (src[i] === ' ' || src[i] === '\t')) i++;
197
-
198
- // Optional quote around the identifier.
199
- let quote = '';
200
- if (src[i] === "'" || src[i] === '"') {
201
- quote = src[i];
202
- i++;
203
- }
194
+ const len = src.length;
195
+ // Optional whitespace (PHP allows spaces/tabs after `<<<`).
196
+ while (i < len && (src[i] === ' ' || src[i] === '\t')) i++;
197
+
198
+ // Optional quote around the identifier.
199
+ let quote = '';
200
+ if (src[i] === "'" || src[i] === '"') {
201
+ quote = src[i];
202
+ i++;
203
+ }
204
204
 
205
- if (i >= len || !isIdentStart(src[i])) return -1;
206
- const idStart = i;
207
- while (i < len && isIdent(src[i])) i++;
208
- const id = src.slice(idStart, i);
205
+ if (i >= len || !isIdentStart(src[i])) return -1;
206
+ const idStart = i;
207
+ while (i < len && isIdent(src[i])) i++;
208
+ const id = src.slice(idStart, i);
209
209
 
210
- if (quote) {
211
- if (src[i] !== quote) return -1;
210
+ if (quote) {
211
+ if (src[i] !== quote) return -1;
212
+ i++;
213
+ }
214
+
215
+ // After the identifier, the line must end immediately — PHP disallows trailing whitespace,
216
+ // but we tolerate `\r` so `\r\n` endings still work.
217
+ while (i < len && src[i] === '\r') i++;
218
+ if (src[i] !== '\n') return -1;
212
219
  i++;
213
- }
214
-
215
- // After the identifier, the line must end immediately — PHP disallows trailing whitespace,
216
- // but we tolerate `\r` so `\r\n` endings still work.
217
- while (i < len && src[i] === '\r') i++;
218
- if (src[i] !== '\n') return -1;
219
- i++;
220
-
221
- // Find a line that starts with optional indentation,
222
- // then the identifier followed by a non-identifier character
223
- // (PHP 7.3 flexible syntax).
224
- while (i < len) {
225
- let j = i;
226
- while (j < len && (src[j] === ' ' || src[j] === '\t')) j++;
227
- if (src.startsWith(id, j)) {
228
- const k = j + id.length;
229
- if (k >= len || !isIdent(src[k])) return k;
220
+
221
+ // Find a line that starts with optional indentation,
222
+ // then the identifier followed by a non-identifier character
223
+ // (PHP 7.3 flexible syntax).
224
+ while (i < len) {
225
+ let j = i;
226
+ while (j < len && (src[j] === ' ' || src[j] === '\t')) j++;
227
+ if (src.startsWith(id, j)) {
228
+ const k = j + id.length;
229
+ if (k >= len || !isIdent(src[k])) return k;
230
+ }
231
+ const nl = src.indexOf('\n', i);
232
+ if (nl === -1) return len;
233
+ i = nl + 1;
230
234
  }
231
- const nl = src.indexOf('\n', i);
232
- if (nl === -1) return len;
233
- i = nl + 1;
234
- }
235
- return len;
235
+ return len;
236
236
  }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * PHP string-literal harvester — the optional third pass.
3
+ *
4
+ * Given the source and the PHP islands already found by `findIslands()`, this locates the byte ranges of every
5
+ * string literal *value* inside PHP code that is eligible for class sorting, applying these structural rules:
6
+ *
7
+ * - Only the *value* of a `key => value` pair is eligible; array keys are never sorted.
8
+ * - Bare list-style elements (`['a', 'b']`) and scalar assignments (`const X = '...'`) are values.
9
+ * - Strings that are part of a concatenation expression (`'btn-' . $v`) are skipped — a literal joined to dynamic
10
+ * code may be a partial class fragment, and reordering it would corrupt the rendered string.
11
+ * - Double-quoted strings containing interpolation (`"p-4 {$x}"`) are skipped, mirroring the HTML side's
12
+ * conservatism around dynamic content.
13
+ * - Strings whose body contains a backslash escape are skipped, so escape sequences can never be mangled.
14
+ * - Heredoc/nowdoc and backtick (shell-exec) strings are never harvested.
15
+ *
16
+ * This pass is opt-in per file (the caller decides which files are class-string holders); it does NOT judge whether
17
+ * a given string "looks like" Tailwind classes — within a matched file, every eligible value is sorted.
18
+ *
19
+ * @see islands.ts - first pass; produces the islands consumed here.
20
+ * @see transform.ts - splices the sorted strings back via the shared byte-replacement path.
21
+ */
22
+
23
+ import type { Island } from './islands.ts';
24
+
25
+ /**
26
+ * Inner byte range of a sortable string literal — the span *between* the quotes, in original-source offsets.
27
+ */
28
+ export interface PhpStringRange {
29
+ /**
30
+ * Offset of the first character inside the quotes.
31
+ */
32
+ start: number;
33
+ /**
34
+ * Offset just past the last character inside the quotes (exclusive).
35
+ */
36
+ end: number;
37
+ }
38
+
39
+ const isIdentStart = (c: string) => /[A-Za-z_€-￿]/.test(c);
40
+ const isIdent = (c: string) => /[A-Za-z0-9_€-￿]/.test(c);
41
+
42
+ interface Token {
43
+ kind: 'string' | 'arrow' | 'dot' | 'other';
44
+ /**
45
+ * For `string` tokens: inner range and whether it must be skipped.
46
+ */
47
+ start?: number;
48
+ end?: number;
49
+ skip?: boolean;
50
+ }
51
+
52
+ /**
53
+ * Find every sortable string-literal value within the given PHP islands.
54
+ *
55
+ * @param src Original template source.
56
+ * @param islands Island ranges from `findIslands()`.
57
+ * @returns Inner ranges of eligible string values, in document order.
58
+ *
59
+ * @example
60
+ * const islands = findIslands(`<?php $x = 'z a'; ?>`);
61
+ * findSortablePhpStrings(`<?php $x = 'z a'; ?>`, islands); // [{ start: 11, end: 14 }]
62
+ */
63
+ export function findSortablePhpStrings(src: string, islands: Island[]): PhpStringRange[] {
64
+ const ranges: PhpStringRange[] = [];
65
+ for (const isl of islands) {
66
+ const tokens = tokenizeIsland(src, isl.start, isl.end);
67
+ for (let k = 0; k < tokens.length; k++) {
68
+ const token = tokens[k];
69
+ if (token.kind !== 'string' || token.skip) continue;
70
+
71
+ const next = tokens[k + 1];
72
+ const prev = tokens[k - 1];
73
+
74
+ // Array key (`'left' => ...`) — never sorted.
75
+ if (next && next.kind === 'arrow') continue;
76
+ // Part of a concatenation expression (`'btn-' . $v` / `$v . 'suffix'`).
77
+ if ((next && next.kind === 'dot') || (prev && prev.kind === 'dot')) continue;
78
+
79
+ ranges.push({ start: token.start!, end: token.end! });
80
+ }
81
+ }
82
+ return ranges;
83
+ }
84
+
85
+ /**
86
+ * Tokenize one PHP island into the minimal token stream needed for value classification: string literals
87
+ * (with their inner range and a skip flag), the `=>` arrow, the `.` concatenation operator, and a coalesced `other`
88
+ * marker for everything else. Whitespace and comments are dropped so adjacency is judged across them.
89
+ */
90
+ function tokenizeIsland(src: string, start: number, end: number): Token[] {
91
+ const tokens: Token[] = [];
92
+ const pushOther = () => {
93
+ if (tokens.length === 0 || tokens[tokens.length - 1].kind !== 'other') tokens.push({ kind: 'other' });
94
+ };
95
+
96
+ let i = start;
97
+ while (i < end) {
98
+ const c = src[i];
99
+
100
+ // Whitespace.
101
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f' || c === '\v') {
102
+ i++;
103
+ continue;
104
+ }
105
+
106
+ // Line comments (`//`, `#`) — but `#[` opens a PHP 8 attribute, which is code.
107
+ if (c === '/' && src[i + 1] === '/') {
108
+ i = skipLineComment(src, i + 2, end);
109
+ continue;
110
+ }
111
+ if (c === '#' && src[i + 1] !== '[') {
112
+ i = skipLineComment(src, i + 1, end);
113
+ continue;
114
+ }
115
+
116
+ // Block comment.
117
+ if (c === '/' && src[i + 1] === '*') {
118
+ const close = src.indexOf('*/', i + 2);
119
+ i = close === -1 || close + 2 > end ? end : close + 2;
120
+ continue;
121
+ }
122
+
123
+ // Heredoc / nowdoc — never harvested; skip the whole construct.
124
+ if (c === '<' && src[i + 1] === '<' && src[i + 2] === '<') {
125
+ const here = skipHeredoc(src, i + 3, end);
126
+ if (here !== -1) {
127
+ i = here;
128
+ pushOther();
129
+ continue;
130
+ }
131
+ }
132
+
133
+ // Quoted strings.
134
+ if (c === "'") {
135
+ const close = scanQuoted(src, i + 1, "'", end);
136
+ const inner = src.slice(i + 1, close);
137
+ tokens.push({ kind: 'string', start: i + 1, end: close, skip: inner.includes('\\') });
138
+ i = close + 1;
139
+ continue;
140
+ }
141
+ if (c === '"') {
142
+ const close = scanQuoted(src, i + 1, '"', end);
143
+ const inner = src.slice(i + 1, close);
144
+ // Skip interpolation (any unescaped `$`) and escapes.
145
+ tokens.push({ kind: 'string', start: i + 1, end: close, skip: hasInterpolationOrEscape(inner) });
146
+ i = close + 1;
147
+ continue;
148
+ }
149
+ if (c === '`') {
150
+ // Shell-exec string — never a class list.
151
+ i = scanQuoted(src, i + 1, '`', end) + 1;
152
+ pushOther();
153
+ continue;
154
+ }
155
+
156
+ // Operators that matter for classification.
157
+ if (c === '=' && src[i + 1] === '>') {
158
+ tokens.push({ kind: 'arrow' });
159
+ i += 2;
160
+ continue;
161
+ }
162
+ if (c === '.') {
163
+ tokens.push({ kind: 'dot' });
164
+ i++;
165
+ continue;
166
+ }
167
+
168
+ // Everything else (identifiers, punctuation, the `<?php`/`?>` tags themselves).
169
+ pushOther();
170
+ if (isIdentStart(c)) {
171
+ i++;
172
+ while (i < end && isIdent(src[i])) i++;
173
+ } else {
174
+ i++;
175
+ }
176
+ }
177
+
178
+ return tokens;
179
+ }
180
+
181
+ /**
182
+ * Scan a quoted string body; `i` is just past the open quote. Returns the offset of the closing quote (or `end`).
183
+ */
184
+ function scanQuoted(src: string, i: number, quote: string, end: number): number {
185
+ while (i < end) {
186
+ const c = src[i];
187
+ if (c === '\\') {
188
+ i += 2;
189
+ continue;
190
+ }
191
+ if (c === quote) return i;
192
+ i++;
193
+ }
194
+ return end;
195
+ }
196
+
197
+ /**
198
+ * Skip a `//`/`#` line comment body, ending at a newline or `?>`. Returns the offset to resume scanning from.
199
+ */
200
+ function skipLineComment(src: string, i: number, end: number): number {
201
+ while (i < end) {
202
+ if (src[i] === '\n') return i + 1;
203
+ if (src[i] === '?' && src[i + 1] === '>') return i;
204
+ i++;
205
+ }
206
+ return end;
207
+ }
208
+
209
+ /**
210
+ * True if a double-quoted body contains interpolation (an unescaped `$`) or any escape sequence.
211
+ */
212
+ function hasInterpolationOrEscape(body: string): boolean {
213
+ for (let i = 0; i < body.length; i++) {
214
+ const c = body[i];
215
+ if (c === '\\') return true;
216
+ if (c === '$') return true;
217
+ }
218
+ return false;
219
+ }
220
+
221
+ /**
222
+ * Skip a heredoc/nowdoc; `i` is just past `<<<`. Returns the offset past the closing identifier line,
223
+ * or -1 if `<<<` isn't followed by a valid heredoc identifier. Mirrors the boundary rules of the island lexer.
224
+ */
225
+ function skipHeredoc(src: string, i: number, end: number): number {
226
+ while (i < end && (src[i] === ' ' || src[i] === '\t')) i++;
227
+
228
+ let quote = '';
229
+ if (src[i] === "'" || src[i] === '"') {
230
+ quote = src[i];
231
+ i++;
232
+ }
233
+
234
+ if (i >= end || !isIdentStart(src[i])) return -1;
235
+ const idStart = i;
236
+ while (i < end && isIdent(src[i])) i++;
237
+ const id = src.slice(idStart, i);
238
+
239
+ if (quote) {
240
+ if (src[i] !== quote) return -1;
241
+ i++;
242
+ }
243
+
244
+ while (i < end && src[i] === '\r') i++;
245
+ if (src[i] !== '\n') return -1;
246
+ i++;
247
+
248
+ while (i < end) {
249
+ let j = i;
250
+ while (j < end && (src[j] === ' ' || src[j] === '\t')) j++;
251
+ if (src.startsWith(id, j)) {
252
+ const k = j + id.length;
253
+ if (k >= end || !isIdent(src[k])) return k;
254
+ }
255
+ const nl = src.indexOf('\n', i);
256
+ if (nl === -1 || nl >= end) return end;
257
+ i = nl + 1;
258
+ }
259
+ return end;
260
+ }
package/src/sorter.ts CHANGED
@@ -12,19 +12,19 @@ import type { SortFn } from './transform.ts';
12
12
  * Options for constructing the official Tailwind sorter.
13
13
  */
14
14
  export interface SorterOptions {
15
- /**
16
- * Tailwind v4 CSS entry point (`@import "tailwindcss"`, `@theme`, etc.).
17
- */
18
- stylesheet: string;
19
- /**
20
- * Base directory for resolving relative paths. Default: `cwd`.
21
- */
22
- base?: string;
15
+ /**
16
+ * Tailwind v4 CSS entry point.
17
+ */
18
+ stylesheet: string;
19
+ /**
20
+ * Base directory for resolving relative paths. Default: `cwd`.
21
+ */
22
+ base?: string;
23
23
  }
24
24
 
25
25
  /**
26
- * Create a `SortFn` backed by the official `prettier-plugin-tailwindcss` sorting engine, configured with
27
- * the project's Tailwind v4 stylesheet so custom `@theme` tokens and `@utility` classes sort correctly.
26
+ * Create a `SortFn` backed by the official `prettier-plugin-tailwindcss` sorting engine,
27
+ * configured with the project's Tailwind v4 stylesheet so custom tokens and classes sort correctly.
28
28
  *
29
29
  * Requires `prettier-plugin-tailwindcss` >= 0.8 (the `/sorter` entrypoint).
30
30
  *
@@ -32,14 +32,13 @@ export interface SorterOptions {
32
32
  * @returns A synchronous `SortFn` for use with `transform()`.
33
33
  */
34
34
  export async function createTailwindSortFn(opts: SorterOptions): Promise<SortFn> {
35
- // Dynamic import so the core package works without the dependency
36
- // installed (e.g., when only running tests with a mock sorter).
37
- const { createSorter } = await import('prettier-plugin-tailwindcss/sorter');
35
+ // Dynamic import so the core package works without the dependency installed.
36
+ const { createSorter } = await import('prettier-plugin-tailwindcss/sorter');
38
37
 
39
- const sorter = await createSorter({
40
- base: opts.base ?? process.cwd(),
41
- stylesheetPath: opts.stylesheet,
42
- });
38
+ const sorter = await createSorter({
39
+ base: opts.base ?? process.cwd(),
40
+ stylesheetPath: opts.stylesheet,
41
+ });
43
42
 
44
- return (classes: string[]) => sorter.sortClassLists([classes])[0];
43
+ return (classes: string[]) => sorter.sortClassLists([classes])[0];
45
44
  }