diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index 145c33052805..1bc7b8a76f35 100644 --- a/src/lib/expandApplyAtRules.js +++ b/src/lib/expandApplyAtRules.js @@ -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.` ) diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 63caa54398c7..c3385403ff30 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -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 = [ { @@ -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 } @@ -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) diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 9461b76fa512..332e68c223b6 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -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('{')) { @@ -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) { diff --git a/tests/apply.test.js b/tests/apply.test.js index 5352ec92c248..a015262c3128 100644 --- a/tests/apply.test.js +++ b/tests/apply.test.js @@ -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: '
' }], + } + + 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`
` }], + } + + 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`
` }], diff --git a/tests/sortClassList.test.js b/tests/sortClassList.test.js new file mode 100644 index 000000000000..f4e0419516b3 --- /dev/null +++ b/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