diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index 89f8515d061b..523a143a8905 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -507,7 +507,7 @@ function extractArbitraryProperty(classCandidate, context) { return null } - let sort = context.offsets.arbitraryProperty() + let sort = context.offsets.arbitraryProperty(classCandidate) return [ [ diff --git a/src/lib/offsets.js b/src/lib/offsets.js index 0ecc04b77b84..522e2ab33289 100644 --- a/src/lib/offsets.js +++ b/src/lib/offsets.js @@ -24,6 +24,8 @@ import { remapBitfield } from './remap-bitfield.js' * @property {bigint} variants Dynamic size. 1 bit per registered variant. 0n means no variants * @property {bigint} parallelIndex Rule index for the parallel variant. 0 if not applicable. * @property {bigint} index Index of the rule / utility in its given *parent* layer. Monotonically increasing. + * @property {bigint} propertyOffset Offset for the arbitrary property. Only valid after sorting. + * @property {string} property Name/Value of the arbitrary property. * @property {VariantOption[]} options Some information on how we can sort arbitrary variants */ @@ -88,17 +90,21 @@ export class Offsets { variants: 0n, parallelIndex: 0n, index: this.offsets[layer]++, + propertyOffset: 0n, + property: '', options: [], } } /** + * @param {string} name * @returns {RuleOffset} */ - arbitraryProperty() { + arbitraryProperty(name) { return { ...this.create('utilities'), arbitrary: 1n, + property: name, } } @@ -262,6 +268,11 @@ export class Offsets { return a.arbitrary - b.arbitrary } + // Always sort arbitrary properties alphabetically + if (a.propertyOffset !== b.propertyOffset) { + return a.propertyOffset - b.propertyOffset + } + // Sort utilities, components, etc… in the order they were registered return a.index - b.index } @@ -320,14 +331,62 @@ export class Offsets { }) } + /** + * @template T + * @param {[RuleOffset, T][]} list + * @returns {[RuleOffset, T][]} + */ + sortArbitraryProperties(list) { + // Collect all known arbitrary properties + let known = new Set() + + for (let [offset] of list) { + if (offset.arbitrary === 1n) { + known.add(offset.property) + } + } + + // No arbitrary properties? Nothing to do. + if (known.size === 0) { + return list + } + + // Sort the properties alphabetically + let properties = Array.from(known).sort() + + // Create a map from the property name to its offset + let offsets = new Map() + + let offset = 1n + for (let property of properties) { + offsets.set(property, offset++) + } + + // Apply the sorted offsets to the list + return list.map((item) => { + let [offset, rule] = item + + offset = { + ...offset, + propertyOffset: offsets.get(offset.property) ?? 0n, + } + + return [offset, rule] + }) + } + /** * @template T * @param {[RuleOffset, T][]} list * @returns {[RuleOffset, T][]} */ sort(list) { + // Sort arbitrary variants so they're in alphabetical order list = this.remapArbitraryVariantOffsets(list) + // Sort arbitrary properties so they're in alphabetical order + list = this.sortArbitraryProperties(list) + return list.sort(([a], [b]) => bigSign(this.compare(a, b))) } } diff --git a/tests/getSortOrder.test.js b/tests/getSortOrder.test.js index b15dc43aeaa0..42d8909101f0 100644 --- a/tests/getSortOrder.test.js +++ b/tests/getSortOrder.test.js @@ -217,3 +217,26 @@ it('Sorting is unchanged when multiple candidates share the same rule / object', expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output) } }) + +it('sorts arbitrary values across one or more class lists consistently', () => { + let classes = [ + ['[--fg:#fff]', '[--fg:#fff]'], + ['[--bg:#111] [--bg_hover:#000] [--fg:#fff]', '[--bg:#111] [--bg_hover:#000] [--fg:#fff]'], + ] + + let config = { + theme: {}, + } + + // Same context, different class lists + let context = createContext(resolveConfig(config)) + for (const [input, output] of classes) { + expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output) + } + + // Different context, different class lists + for (const [input, output] of classes) { + context = createContext(resolveConfig(config)) + expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output) + } +}) diff --git a/tests/variants.test.js b/tests/variants.test.js index bccebf8a593f..4386edfa0ee8 100644 --- a/tests/variants.test.js +++ b/tests/variants.test.js @@ -37,18 +37,18 @@ test('order matters and produces different behaviour', () => { return run('@tailwind utilities', config).then((result) => { expect(result.css).toMatchFormattedCss(css` - .file\:hover\:\[--value\:2\]:hover::-webkit-file-upload-button { - --value: 2; - } - .file\:hover\:\[--value\:2\]:hover::file-selector-button { - --value: 2; - } .hover\:file\:\[--value\:1\]::-webkit-file-upload-button:hover { --value: 1; } .hover\:file\:\[--value\:1\]::file-selector-button:hover { --value: 1; } + .file\:hover\:\[--value\:2\]:hover::-webkit-file-upload-button { + --value: 2; + } + .file\:hover\:\[--value\:2\]:hover::file-selector-button { + --value: 2; + } `) }) })