Skip to content

Commit a441d91

Browse files
committed
expose sortClassList on the context
This function will be used by the `prettier-plugin-tailwindcss` plugin, this way the sorting happens within Tailwind CSS itself adn the `prettier-plugin-tailwindcss` plugin doesn't have to use internal / private APIs. The signature looks like this: ```ts function sortClassList(classes: string[]): string[] ``` E.g.: ```js let sortedClasses = context.sortClassList(['p-1', 'm-1', 'container']) ```
1 parent 5807529 commit a441d91

File tree

5 files changed

+165
-7
lines changed

5 files changed

+165
-7
lines changed

src/lib/expandApplyAtRules.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,12 @@ function processApply(root, context) {
161161
}
162162

163163
for (let applyCandidate of applyCandidates) {
164+
if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) {
165+
// TODO: Link to specific documentation page with error code.
166+
throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
167+
}
168+
164169
if (!applyClassCache.has(applyCandidate)) {
165-
if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) {
166-
// TODO: Link to specific documentation page with error code.
167-
throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
168-
}
169170
throw apply.error(
170171
`The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
171172
)

src/lib/generateRules.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ function applyVariant(variant, matches, context) {
234234
// For example:
235235
// .sm:underline {} is a variant of something in the utilities layer
236236
// .sm:container {} is a variant of the container component
237-
clone.nodes[0].raws.tailwind = { parentLayer: meta.layer }
237+
clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer }
238238

239239
let withOffset = [
240240
{
@@ -387,7 +387,7 @@ function splitWithSeparator(input, separator) {
387387

388388
function* recordCandidates(matches, classCandidate) {
389389
for (const match of matches) {
390-
match[1].raws.tailwind = { classCandidate }
390+
match[1].raws.tailwind = { ...match[1].raws.tailwind, classCandidate }
391391

392392
yield match
393393
}
@@ -517,6 +517,8 @@ function* resolveMatches(candidate, context) {
517517
}
518518

519519
for (let match of matches) {
520+
match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
521+
520522
// Apply final format selector
521523
if (match[0].collectedFormats) {
522524
let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats)

src/lib/setupContextUtils.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ import { toPath } from '../util/toPath'
1919
import log from '../util/log'
2020
import negateValue from '../util/negateValue'
2121
import isValidArbitraryValue from '../util/isValidArbitraryValue'
22+
import { generateRules } from './generateRules'
23+
24+
function prefix(context, selector) {
25+
let prefix = context.tailwindConfig.prefix
26+
return typeof prefix === 'function' ? prefix(selector) : prefix + selector
27+
}
2228

2329
function parseVariantFormatString(input) {
2430
if (input.includes('{')) {
@@ -733,9 +739,43 @@ function registerPlugins(plugins, context) {
733739
}
734740
}
735741

742+
// A list of utilities that are used by certain Tailwind CSS utilities but
743+
// that don't exist on their own. This will result in them "not existing" and
744+
// sorting could be weird since you still require them in order to make the
745+
// host utitlies work properly. (Thanks Biology)
746+
let parasiteUtilities = new Set([prefix(context, 'group'), prefix(context, 'peer')])
747+
context.sortClassList = function sortClassList(classes) {
748+
let sortedClassNames = new Map()
749+
for (let [sort, rule] of generateRules(new Set(classes), context)) {
750+
if (sortedClassNames.has(rule.raws.tailwind.candidate)) continue
751+
sortedClassNames.set(rule.raws.tailwind.candidate, sort)
752+
}
753+
754+
return classes
755+
.map((className) => {
756+
let order = sortedClassNames.get(className) ?? null
757+
758+
if (order === null && parasiteUtilities.has(className)) {
759+
// This will make sure that it is at the very beginning of the
760+
// `components` layer which technically means 'before any
761+
// components'.
762+
order = context.layerOrder.components
763+
}
764+
765+
return [className, order]
766+
})
767+
.sort(([, a], [, z]) => {
768+
if (a === z) return 0
769+
if (a === null) return -1
770+
if (z === null) return 1
771+
return bigSign(a - z)
772+
})
773+
.map(([className]) => className)
774+
}
775+
736776
// Generate a list of strings for autocompletion purposes, e.g.
737777
// ['uppercase', 'lowercase', ...]
738-
context.getClassList = function () {
778+
context.getClassList = function getClassList() {
739779
let output = []
740780

741781
for (let util of classList) {

tests/apply.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,51 @@ test('@apply error when using a prefixed .group utility', async () => {
249249
)
250250
})
251251

252+
test('@apply error when using .peer utility', async () => {
253+
let config = {
254+
darkMode: 'class',
255+
content: [{ raw: '<div class="foo"></div>' }],
256+
}
257+
258+
let input = css`
259+
@tailwind components;
260+
@tailwind utilities;
261+
262+
@layer components {
263+
.foo {
264+
@apply peer;
265+
}
266+
}
267+
`
268+
269+
await expect(run(input, config)).rejects.toThrowError(
270+
`@apply should not be used with the 'peer' utility`
271+
)
272+
})
273+
274+
test('@apply error when using a prefixed .peer utility', async () => {
275+
let config = {
276+
prefix: 'tw-',
277+
darkMode: 'class',
278+
content: [{ raw: html`<div class="foo"></div>` }],
279+
}
280+
281+
let input = css`
282+
@tailwind components;
283+
@tailwind utilities;
284+
285+
@layer components {
286+
.foo {
287+
@apply tw-peer;
288+
}
289+
}
290+
`
291+
292+
await expect(run(input, config)).rejects.toThrowError(
293+
`@apply should not be used with the 'tw-peer' utility`
294+
)
295+
})
296+
252297
test('@apply classes from outside a @layer', async () => {
253298
let config = {
254299
content: [{ raw: html`<div class="foo bar baz font-bold"></div>` }],

tests/sortClassList.test.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import resolveConfig from '../src/public/resolve-config'
2+
import { createContext } from '../src/lib/setupContextUtils'
3+
4+
it.each([
5+
// Utitlies
6+
['px-3 p-1 py-3', 'p-1 px-3 py-3'],
7+
8+
// Utitlies and components
9+
['px-4 container', 'container px-4'],
10+
11+
// Utilities with variants
12+
['px-3 focus:hover:p-3 hover:p-1 py-3', 'px-3 py-3 hover:p-1 focus:hover:p-3'],
13+
14+
// Components with variants
15+
['hover:container container', 'container hover:container'],
16+
17+
// Components and utilities with variants
18+
[
19+
'focus:hover:container hover:underline hover:container p-1',
20+
'p-1 hover:container hover:underline focus:hover:container',
21+
],
22+
23+
// Leave user css order alone, and move to the front
24+
['b p-1 a', 'b a p-1'],
25+
['hover:b focus:p-1 a', 'hover:b a focus:p-1'],
26+
27+
// Add special treatment for `group` and `peer`
28+
['a peer container underline', 'a peer container underline'],
29+
])('should sort "%s" based on the order we generate them in to "%s"', (input, output) => {
30+
let config = {}
31+
let context = createContext(resolveConfig(config))
32+
expect(context.sortClassList(input.split(' ')).join(' ')).toEqual(output)
33+
})
34+
35+
it.each([
36+
// Utitlies
37+
['tw-px-3 tw-p-1 tw-py-3', 'tw-p-1 tw-px-3 tw-py-3'],
38+
39+
// Utitlies and components
40+
['tw-px-4 tw-container', 'tw-container tw-px-4'],
41+
42+
// Utilities with variants
43+
[
44+
'tw-px-3 focus:hover:tw-p-3 hover:tw-p-1 tw-py-3',
45+
'tw-px-3 tw-py-3 hover:tw-p-1 focus:hover:tw-p-3',
46+
],
47+
48+
// Components with variants
49+
['hover:tw-container tw-container', 'tw-container hover:tw-container'],
50+
51+
// Components and utilities with variants
52+
[
53+
'focus:hover:tw-container hover:tw-underline hover:tw-container tw-p-1',
54+
'tw-p-1 hover:tw-container hover:tw-underline focus:hover:tw-container',
55+
],
56+
57+
// Leave user css order alone, and move to the front
58+
['b tw-p-1 a', 'b a tw-p-1'],
59+
['hover:b focus:tw-p-1 a', 'hover:b a focus:tw-p-1'],
60+
61+
// Add special treatment for `group` and `peer`
62+
['a tw-peer tw-container tw-underline', 'a tw-peer tw-container tw-underline'],
63+
])(
64+
'should sort "%s" with prefixex based on the order we generate them in to "%s"',
65+
(input, output) => {
66+
let config = { prefix: 'tw-' }
67+
let context = createContext(resolveConfig(config))
68+
expect(context.sortClassList(input.split(' ')).join(' ')).toEqual(output)
69+
}
70+
)

0 commit comments

Comments
 (0)