Skip to content

Commit

Permalink
expose sortClassList on the context
Browse files Browse the repository at this point in the history
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'])
```
  • Loading branch information
RobinMalfait committed Feb 10, 2022
1 parent 5807529 commit 4822d62
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 7 deletions.
9 changes: 5 additions & 4 deletions src/lib/expandApplyAtRules.js
Expand Up @@ -161,11 +161,12 @@ function processApply(root, context) {
}

for (let applyCandidate of applyCandidates) {
if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) {
// TODO: Link to specific documentation page with error code.
throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
}

if (!applyClassCache.has(applyCandidate)) {
if ([prefix(context, 'group'), prefix(context, 'peer')].includes(applyCandidate)) {
// TODO: Link to specific documentation page with error code.
throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
}
throw apply.error(
`The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
)
Expand Down
6 changes: 4 additions & 2 deletions src/lib/generateRules.js
Expand Up @@ -234,7 +234,7 @@ function applyVariant(variant, matches, context) {
// For example:
// .sm:underline {} is a variant of something in the utilities layer
// .sm:container {} is a variant of the container component
clone.nodes[0].raws.tailwind = { parentLayer: meta.layer }
clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer }

let withOffset = [
{
Expand Down Expand Up @@ -387,7 +387,7 @@ function splitWithSeparator(input, separator) {

function* recordCandidates(matches, classCandidate) {
for (const match of matches) {
match[1].raws.tailwind = { classCandidate }
match[1].raws.tailwind = { ...match[1].raws.tailwind, classCandidate }

yield match
}
Expand Down Expand Up @@ -517,6 +517,8 @@ function* resolveMatches(candidate, context) {
}

for (let match of matches) {
match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }

// Apply final format selector
if (match[0].collectedFormats) {
let finalFormat = formatVariantSelector('&', ...match[0].collectedFormats)
Expand Down
46 changes: 45 additions & 1 deletion src/lib/setupContextUtils.js
Expand Up @@ -19,6 +19,12 @@ import { toPath } from '../util/toPath'
import log from '../util/log'
import negateValue from '../util/negateValue'
import isValidArbitraryValue from '../util/isValidArbitraryValue'
import { generateRules } from './generateRules'

function prefix(context, selector) {
let prefix = context.tailwindConfig.prefix
return typeof prefix === 'function' ? prefix(selector) : prefix + selector
}

function parseVariantFormatString(input) {
if (input.includes('{')) {
Expand Down Expand Up @@ -733,9 +739,47 @@ function registerPlugins(plugins, context) {
}
}

// A list of utilities that are used by certain Tailwind CSS utilities but
// that don't exist on their own. This will result in them "not existing" and
// sorting could be weird since you still require them in order to make the
// host utitlies work properly. (Thanks Biology)
let parasiteUtilities = new Set([prefix(context, 'group'), prefix(context, 'peer')])
context.sortClassList = function sortClassList(classes) {
let sortedClassNames = new Map()
for (let [sort, rule] of generateRules(new Set(classes), context)) {
if (sortedClassNames.has(rule.raws.tailwind.candidate)) continue
sortedClassNames.set(rule.raws.tailwind.candidate, sort)
}

return classes
.map((className) => {
let order = sortedClassNames.get(className) ?? null

if (order === null && parasiteUtilities.has(className)) {
let variants = className.split(context.tailwindConfig.separator)
variants.pop() // className

order = context.layerOrder.utilities

for (let variant of variants) {
order |= context.variantOrder.get(variant)
}
}

return [className, order]
})
.sort(([, a], [, z]) => {
if (a === z) return 0
if (a === null) return -1
if (z === null) return 1
return bigSign(a - z)
})
.map(([className]) => className)
}

// Generate a list of strings for autocompletion purposes, e.g.
// ['uppercase', 'lowercase', ...]
context.getClassList = function () {
context.getClassList = function getClassList() {
let output = []

for (let util of classList) {
Expand Down
45 changes: 45 additions & 0 deletions tests/apply.test.js
Expand Up @@ -249,6 +249,51 @@ test('@apply error when using a prefixed .group utility', async () => {
)
})

test('@apply error when using .peer utility', async () => {
let config = {
darkMode: 'class',
content: [{ raw: '<div class="foo"></div>' }],
}

let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@apply peer;
}
}
`

await expect(run(input, config)).rejects.toThrowError(
`@apply should not be used with the 'peer' utility`
)
})

test('@apply error when using a prefixed .peer utility', async () => {
let config = {
prefix: 'tw-',
darkMode: 'class',
content: [{ raw: html`<div class="foo"></div>` }],
}

let input = css`
@tailwind components;
@tailwind utilities;
@layer components {
.foo {
@apply tw-peer;
}
}
`

await expect(run(input, config)).rejects.toThrowError(
`@apply should not be used with the 'tw-peer' utility`
)
})

test('@apply classes from outside a @layer', async () => {
let config = {
content: [{ raw: html`<div class="foo bar baz font-bold"></div>` }],
Expand Down
72 changes: 72 additions & 0 deletions tests/sortClassList.test.js
@@ -0,0 +1,72 @@
import resolveConfig from '../src/public/resolve-config'
import { createContext } from '../src/lib/setupContextUtils'

it.each([
// Utitlies
['px-3 p-1 py-3', 'p-1 px-3 py-3'],

// Utitlies and components
['px-4 container', 'container px-4'],

// Utilities with variants
['px-3 focus:hover:p-3 hover:p-1 py-3', 'px-3 py-3 hover:p-1 focus:hover:p-3'],

// Components with variants
['hover:container container', 'container hover:container'],

// Components and utilities with variants
[
'focus:hover:container hover:underline hover:container p-1',
'p-1 hover:container hover:underline focus:hover:container',
],

// Leave user css order alone, and move to the front
['b p-1 a', 'b a p-1'],
['hover:b focus:p-1 a', 'hover:b a focus:p-1'],

// Add special treatment for `group` and `peer`
['a peer container underline', 'a container peer underline'],
])('should sort "%s" based on the order we generate them in to "%s"', (input, output) => {
let config = {}
let context = createContext(resolveConfig(config))
expect(context.sortClassList(input.split(' '))).toEqual(output.split(' '))
})

it.each([
// Utitlies
['tw-px-3 tw-p-1 tw-py-3', 'tw-p-1 tw-px-3 tw-py-3'],

// Utitlies and components
['tw-px-4 tw-container', 'tw-container tw-px-4'],

// Utilities with variants
[
'tw-px-3 focus:hover:tw-p-3 hover:tw-p-1 tw-py-3',
'tw-px-3 tw-py-3 hover:tw-p-1 focus:hover:tw-p-3',
],

// Components with variants
['hover:tw-container tw-container', 'tw-container hover:tw-container'],

// Components and utilities with variants
[
'focus:hover:tw-container hover:tw-underline hover:tw-container tw-p-1',
'tw-p-1 hover:tw-container hover:tw-underline focus:hover:tw-container',
],

// Leave user css order alone, and move to the front
['b tw-p-1 a', 'b a tw-p-1'],
['hover:b focus:tw-p-1 a', 'hover:b a focus:tw-p-1'],

// Add special treatment for `group` and `peer`
['a tw-peer tw-container tw-underline', 'a tw-container tw-peer tw-underline'],
])(
'should sort "%s" with prefixex based on the order we generate them in to "%s"',
(input, output) => {
let config = { prefix: 'tw-' }
let context = createContext(resolveConfig(config))
expect(context.sortClassList(input.split(' '))).toEqual(output.split(' '))
}
)

// TODO: Copy test from above using prefix

0 comments on commit 4822d62

Please sign in to comment.