diff --git a/CHANGELOG.md b/CHANGELOG.md index a637d0352c1e..94ec8646cf92 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)) ## [3.3.3] - 2023-07-13 diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index b15351bffb6d..5fb6bfe0212d 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -882,7 +882,7 @@ function getImportantStrategy(important) { } } -function generateRules(candidates, context) { +function generateRules(candidates, context, isSorting = false) { let allRules = [] let strategy = getImportantStrategy(context.tailwindConfig.important) @@ -917,7 +917,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 b173d419f498..59c261d698b3 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -947,7 +947,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 1916e265f72a..3cfc03e6773e 100644 --- a/tests/getSortOrder.test.js +++ b/tests/getSortOrder.test.js @@ -181,3 +181,42 @@ it('sorts based on first occurence 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) + } +})