Skip to content

Commit

Permalink
Expose context.sortClassList(classes) (#7412)
Browse files Browse the repository at this point in the history
* add prettier-plugin-tailwindcss

This will use the prettier plugin in our tests as well, yay consistency!

* ensure that both `group` and `peer` can't be used in `@apply`

This was only configured for `group`

* 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'])
```

* update changelog

* add sort test for utilities with the important modifier e.g.: `!p-4`
  • Loading branch information
RobinMalfait committed Feb 10, 2022
1 parent 5ea67b0 commit 96d4ce2
Show file tree
Hide file tree
Showing 25 changed files with 274 additions and 90 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -7,7 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Nothing yet!
### Added

- Expose `context.sortClassList(classes)` ([#7412](https://github.com/tailwindlabs/tailwindcss/pull/7412))

## [3.0.19] - 2022-02-07

Expand Down
6 changes: 3 additions & 3 deletions integrations/parcel/tests/integration.test.js
Expand Up @@ -164,7 +164,7 @@ describe.skip('watcher', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="font-bold btn"></div>
<div class="btn font-bold"></div>
`
)

Expand All @@ -190,7 +190,7 @@ describe.skip('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded;
@apply rounded px-2 py-1;
}
}
`
Expand Down Expand Up @@ -222,7 +222,7 @@ describe.skip('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded bg-red-500;
@apply rounded bg-red-500 px-2 py-1;
}
}
`
Expand Down
6 changes: 3 additions & 3 deletions integrations/postcss-cli/tests/integration.test.js
Expand Up @@ -139,7 +139,7 @@ describe('watcher', () => {
})

test('classes are generated when the index.css file changes', async () => {
await writeInputFile('index.html', html`<div class="font-bold btn"></div>`)
await writeInputFile('index.html', html`<div class="btn font-bold"></div>`)

let runningProcess = $('postcss ./src/index.css -o ./dist/main.css -w --verbose')

Expand All @@ -162,7 +162,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded;
@apply rounded px-2 py-1;
}
}
`
Expand Down Expand Up @@ -193,7 +193,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded bg-red-500;
@apply rounded bg-red-500 px-2 py-1;
}
}
`
Expand Down
6 changes: 3 additions & 3 deletions integrations/rollup/tests/integration.test.js
Expand Up @@ -138,7 +138,7 @@ describe('watcher', () => {
})

test(`classes are generated when the index.css file changes`, async () => {
await writeInputFile('index.html', html`<div class="font-bold btn"></div>`)
await writeInputFile('index.html', html`<div class="btn font-bold"></div>`)

let runningProcess = $('rollup -c --watch')
await runningProcess.onStderr(ready)
Expand All @@ -160,7 +160,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded;
@apply rounded px-2 py-1;
}
}
`
Expand Down Expand Up @@ -191,7 +191,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded bg-red-500;
@apply rounded bg-red-500 px-2 py-1;
}
}
`
Expand Down
6 changes: 3 additions & 3 deletions integrations/tailwindcss-cli/tests/integration.test.js
Expand Up @@ -253,7 +253,7 @@ describe('watcher', () => {
})

test('classes are generated when the index.css file changes', async () => {
await writeInputFile('index.html', html`<div class="font-bold btn"></div>`)
await writeInputFile('index.html', html`<div class="btn font-bold"></div>`)

let runningProcess = $('node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css -w')
await runningProcess.onStderr(ready)
Expand All @@ -275,7 +275,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded;
@apply rounded px-2 py-1;
}
}
`
Expand Down Expand Up @@ -306,7 +306,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded bg-red-500;
@apply rounded bg-red-500 px-2 py-1;
}
}
`
Expand Down
6 changes: 3 additions & 3 deletions integrations/vite/tests/integration.test.js
Expand Up @@ -169,7 +169,7 @@ describe('watcher', () => {
'index.html',
html`
<link rel="stylesheet" href="./index.css" />
<div class="font-bold btn"></div>
<div class="btn font-bold"></div>
`
)

Expand All @@ -193,7 +193,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded;
@apply rounded px-2 py-1;
}
}
`
Expand Down Expand Up @@ -224,7 +224,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded bg-red-500;
@apply rounded bg-red-500 px-2 py-1;
}
}
`
Expand Down
6 changes: 3 additions & 3 deletions integrations/webpack-4/tests/integration.test.js
Expand Up @@ -140,7 +140,7 @@ describe('watcher', () => {
})

test(`classes are generated when the index.css file changes`, async () => {
await writeInputFile('index.html', html`<div class="font-bold btn"></div>`)
await writeInputFile('index.html', html`<div class="btn font-bold"></div>`)

let runningProcess = $('webpack --mode=development --watch')

Expand All @@ -164,7 +164,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded;
@apply rounded px-2 py-1;
}
}
`
Expand Down Expand Up @@ -196,7 +196,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded bg-red-500;
@apply rounded bg-red-500 px-2 py-1;
}
}
`
Expand Down
6 changes: 3 additions & 3 deletions integrations/webpack-5/tests/integration.test.js
Expand Up @@ -140,7 +140,7 @@ describe('watcher', () => {
})

test(`classes are generated when the index.css file changes`, async () => {
await writeInputFile('index.html', html`<div class="font-bold btn"></div>`)
await writeInputFile('index.html', html`<div class="btn font-bold"></div>`)

let runningProcess = $('webpack --mode=development --watch')

Expand All @@ -164,7 +164,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded;
@apply rounded px-2 py-1;
}
}
`
Expand Down Expand Up @@ -196,7 +196,7 @@ describe('watcher', () => {
@layer components {
.btn {
@apply px-2 py-1 rounded bg-red-500;
@apply rounded bg-red-500 px-2 py-1;
}
}
`
Expand Down
22 changes: 19 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -77,6 +77,7 @@
"postcss-nested": "5.0.6",
"postcss-selector-parser": "^6.0.9",
"postcss-value-parser": "^4.2.0",
"prettier-plugin-tailwindcss": "^0.1.7",
"quick-lru": "^5.1.1",
"resolve": "^1.22.0"
},
Expand Down
10 changes: 5 additions & 5 deletions src/lib/expandApplyAtRules.js
Expand Up @@ -161,12 +161,12 @@ function processApply(root, context) {
}

for (let applyCandidate of applyCandidates) {
if (!applyClassCache.has(applyCandidate)) {
if (applyCandidate === prefix(context, 'group')) {
// TODO: Link to specific documentation page with error code.
throw apply.error(`@apply should not be used with the '${applyCandidate}' utility`)
}
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)) {
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
42 changes: 41 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,43 @@ 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)) {
// This will make sure that it is at the very beginning of the
// `components` layer which technically means 'before any
// components'.
order = context.layerOrder.components
}

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

0 comments on commit 96d4ce2

Please sign in to comment.