diff --git a/CHANGELOG.md b/CHANGELOG.md index f90e8837d438..f2b0328ae0e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don’t crash when important and parent selectors are equal in `@apply` ([#12112](https://github.com/tailwindlabs/tailwindcss/pull/12112)) - Eliminate irrelevant rules when applying variants ([#12113](https://github.com/tailwindlabs/tailwindcss/pull/12113)) - Improve RegEx parser, reduce possibilities as the key for arbitrary properties ([#12121](https://github.com/tailwindlabs/tailwindcss/pull/12121)) +- Fix sorting of utilities that share multiple candidates ([#12173](https://github.com/tailwindlabs/tailwindcss/pull/12173)) ### Added diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index da893f21a085..ca38d8c03d10 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -877,7 +877,7 @@ function getImportantStrategy(important) { } } -function generateRules(candidates, context) { +function generateRules(candidates, context, isSorting = false) { let allRules = [] let strategy = getImportantStrategy(context.tailwindConfig.important) @@ -912,7 +912,9 @@ function generateRules(candidates, context) { rule = container.nodes[0] } - let newEntry = [sort, rule] + // Note: We have to clone rules during sorting + // so we eliminate some shared mutable state + let newEntry = [sort, isSorting ? rule.clone() : rule] rules.add(newEntry) context.ruleCache.add(newEntry) allRules.push(newEntry) diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index a81475654f7f..65d40a9cbf7f 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -938,7 +938,7 @@ function registerPlugins(plugins, context) { // Sort all classes in order // Non-tailwind classes won't be generated and will be left as `null` - let rules = generateRules(new Set(sorted), context) + let rules = generateRules(new Set(sorted), context, true) rules = context.offsets.sort(rules) let idx = BigInt(parasiteUtilities.length) diff --git a/tests/getSortOrder.test.js b/tests/getSortOrder.test.js index 4f1f281d4198..b15dc43aeaa0 100644 --- a/tests/getSortOrder.test.js +++ b/tests/getSortOrder.test.js @@ -178,3 +178,42 @@ it('sorts based on first occurrence of a candidate / rule', () => { expect(defaultSort(context.getClassOrder(input.split(' ')))).toEqual(output) } }) + +it('Sorting is unchanged when multiple candidates share the same rule / object', () => { + let classes = [ + ['x y', 'x y'], + ['a', 'a'], + ['x y', 'x y'], + ] + + let config = { + theme: {}, + plugins: [ + function ({ addComponents }) { + addComponents({ + '.x': { color: 'red' }, + '.a': { color: 'red' }, + + // This rule matches both the candidate `a` and `y` + // When sorting x and y first we would keep that sort order + // Then sorting `a` we would end up replacing the candidate on the rule + // Thus causing `y` to no longer have a sort order causing it to be sorted + // first by accident + '.y .a': { color: 'red' }, + }) + }, + ], + } + + // 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) + } +})