diff --git a/src/lib/offsets.js b/src/lib/offsets.js index 6fa63d39fb62..6c7a68c4262e 100644 --- a/src/lib/offsets.js +++ b/src/lib/offsets.js @@ -1,6 +1,7 @@ // @ts-check import bigSign from '../util/bigSign' +import { remapBitfield } from './remap-bitfield.js' /** * @typedef {'base' | 'defaults' | 'components' | 'utilities' | 'variants' | 'user'} Layer @@ -243,12 +244,73 @@ export class Offsets { return a.index - b.index } + /** + * Arbitrary variants are recorded in the order they're encountered. + * This means that the order is not stable between environments and sets of content files. + * + * In order to make the order stable, we need to remap the arbitrary variant offsets to + * be in alphabetical order starting from the offset of the first arbitrary variant. + */ + recalculateVariantOffsets() { + // Sort the variants by their name + let variants = Array.from(this.variantOffsets.entries()) + .filter(([v]) => v.startsWith('[')) + .sort(([a], [z]) => a.localeCompare(z)) + + // Sort the list of offsets + // This is not necessarily a discrete range of numbers which is why + // we're using sort instead of creating a range from min/max + let newOffsets = variants + .map(([, offset]) => offset) + .sort((a, z) => bigSign(a - z)) + + // Create a map from the old offsets to the new offsets in the new sort order + /** @type {[bigint, bigint][]} */ + let mapping = variants.map(([, oldOffset], i) => ([ + oldOffset, + newOffsets[i], + ])) + + // Remove any variants that will not move letting us skip + // remapping if everything happens to be in order + return mapping.filter(([a, z]) => a !== z) + } + + /** + * @template T + * @param {[RuleOffset, T][]} list + * @returns {[RuleOffset, T][]} + */ + remapArbitraryVariantOffsets(list) { + let mapping = this.recalculateVariantOffsets() + + // No arbitrary variants? Nothing to do. + // Everyhing already in order? Nothing to do. + if (mapping.length === 0) { + return list + } + + // Remap every variant offset in the list + return list.map(item => { + let [offset, rule] = item + + offset = { + ...offset, + variants: remapBitfield(offset.variants, mapping), + } + + return [offset, rule] + }) + } + /** * @template T * @param {[RuleOffset, T][]} list * @returns {[RuleOffset, T][]} */ sort(list) { + list = this.remapArbitraryVariantOffsets(list) + return list.sort(([a], [b]) => bigSign(this.compare(a, b))) } } diff --git a/src/lib/remap-bitfield.js b/src/lib/remap-bitfield.js new file mode 100644 index 000000000000..ffdfb3b403fa --- /dev/null +++ b/src/lib/remap-bitfield.js @@ -0,0 +1,82 @@ +// @ts-check + +/** + * We must remap all the old bits to new bits for each set variant + * Only arbitrary variants are considered as those are the only + * ones that need to be re-sorted at this time + * + * An iterated process that removes and sets individual bits simultaneously + * will not work because we may have a new bit that is also a later old bit + * This means that we would be removing a previously set bit which we don't + * want to do + * + * For example (assume `bN` = `1< { `) }) }) + +it('Arbitrary variants are ordered alphabetically', () => { + let config = { + content: [ + { + raw: html` +
+
+
+
+
+
+ `, + }, + ], + corePlugins: { preflight: false }, + } + + let input = css` + @tailwind utilities; + ` + + return run(input, config).then((result) => { + expect(result.css).toMatchFormattedCss(css` + .\[\&\:\:a\]\:underline::a { + text-decoration-line: underline; + } + .\[\&\:\:b\]\:underline::b { + text-decoration-line: underline; + } + .\[\&\:\:c\]\:underline::c { + text-decoration-line: underline; + } + `) + }) +}) diff --git a/tests/variants.test.js b/tests/variants.test.js index d6022b8675ab..33b5d2ffe899 100644 --- a/tests/variants.test.js +++ b/tests/variants.test.js @@ -1122,25 +1122,25 @@ test('arbitrary variant selectors should not re-order scrollbar pseudo classes', let result = await run(input, config) expect(result.css).toMatchFormattedCss(css` - .\[\&\:\:-webkit-scrollbar\:hover\]\:underline::-webkit-scrollbar:hover { + .\[\&\:\:-webkit-resizer\:hover\]\:underline::-webkit-resizer:hover { text-decoration-line: underline; } .\[\&\:\:-webkit-scrollbar-button\:hover\]\:underline::-webkit-scrollbar-button:hover { text-decoration-line: underline; } - .\[\&\:\:-webkit-scrollbar-thumb\:hover\]\:underline::-webkit-scrollbar-thumb:hover { + .\[\&\:\:-webkit-scrollbar-corner\:hover\]\:underline::-webkit-scrollbar-corner:hover { text-decoration-line: underline; } - .\[\&\:\:-webkit-scrollbar-track\:hover\]\:underline::-webkit-scrollbar-track:hover { + .\[\&\:\:-webkit-scrollbar-thumb\:hover\]\:underline::-webkit-scrollbar-thumb:hover { text-decoration-line: underline; } .\[\&\:\:-webkit-scrollbar-track-piece\:hover\]\:underline::-webkit-scrollbar-track-piece:hover { text-decoration-line: underline; } - .\[\&\:\:-webkit-scrollbar-corner\:hover\]\:underline::-webkit-scrollbar-corner:hover { + .\[\&\:\:-webkit-scrollbar-track\:hover\]\:underline::-webkit-scrollbar-track:hover { text-decoration-line: underline; } - .\[\&\:\:-webkit-resizer\:hover\]\:underline::-webkit-resizer:hover { + .\[\&\:\:-webkit-scrollbar\:hover\]\:underline::-webkit-scrollbar:hover { text-decoration-line: underline; } `)