@@ -12,10 +12,13 @@ import { keyPathToCssProperty } from './compat/apply-config-to-theme'
1212import type { DesignSystem } from './design-system'
1313import * as SelectorParser from './selector-parser'
1414import {
15+ computeUtilityProperties ,
1516 computeUtilitySignature ,
1617 computeVariantSignature ,
1718 preComputedUtilities ,
1819 preComputedVariants ,
20+ SignatureFeatures ,
21+ staticUtilitiesByPropertyAndValue ,
1922 type SignatureOptions ,
2023} from './signatures'
2124import type { Writable } from './types'
@@ -129,7 +132,172 @@ export function canonicalizeCandidates(
129132 for ( let candidate of candidates ) {
130133 result . add ( cache . get ( candidate ) )
131134 }
135+
136+ return result . size <= 1 || ! ( canonicalizeOptions . features & Features . CollapseUtilities )
137+ ? Array . from ( result )
138+ : collapseCandidates ( canonicalizeOptions , Array . from ( result ) )
139+ }
140+
141+ function collapseCandidates ( options : InternalCanonicalizeOptions , candidates : string [ ] ) : string [ ] {
142+ if ( candidates . length <= 1 ) return candidates
143+
144+ // To keep things simple, we group candidates such that we only collapse
145+ // candidates with the same variants and important modifier together.
146+ let groups = new DefaultMap ( ( _before : string ) => {
147+ return new DefaultMap ( ( _after : string ) => {
148+ return new Set < string > ( )
149+ } )
150+ } )
151+
152+ let prefix = options . designSystem . theme . prefix ? `${ options . designSystem . theme . prefix } :` : ''
153+
154+ for ( let candidate of candidates ) {
155+ let variants = segment ( candidate , ':' )
156+ let utility = variants . pop ( ) !
157+
158+ let important = utility . endsWith ( '!' )
159+ if ( important ) {
160+ utility = utility . slice ( 0 , - 1 )
161+ }
162+
163+ let before = variants . length > 0 ? `${ variants . join ( ':' ) } :` : ''
164+ let after = important ? '!' : ''
165+
166+ // Group by variants and important flag
167+ groups . get ( before ) . get ( after ) . add ( `${ prefix } ${ utility } ` )
168+ }
169+
170+ let result = new Set < string > ( )
171+ for ( let [ before , group ] of groups . entries ( ) ) {
172+ for ( let [ after , candidates ] of group . entries ( ) ) {
173+ for ( let candidate of collapseGroup ( Array . from ( candidates ) ) ) {
174+ // Drop the prefix if we had one, because the prefix is already there as
175+ // part of the variants.
176+ if ( prefix && candidate . startsWith ( prefix ) ) {
177+ candidate = candidate . slice ( prefix . length )
178+ }
179+
180+ result . add ( `${ before } ${ candidate } ${ after } ` )
181+ }
182+ }
183+ }
184+
132185 return Array . from ( result )
186+
187+ function collapseGroup ( candidates : string [ ] ) {
188+ let signatureOptions = options . signatureOptions
189+ let computeUtilitiesPropertiesLookup = computeUtilityProperties . get ( signatureOptions )
190+ let staticUtilities = staticUtilitiesByPropertyAndValue . get ( signatureOptions )
191+
192+ // For each candidate, compute the used properties and values. E.g.: `mt-1` → `margin-top` → `0.25rem`
193+ //
194+ // NOTE: Currently assuming we are dealing with static utilities only. This
195+ // will change the moment we have `@utility` for most built-ins.
196+ let candidatePropertiesValues = candidates . map ( ( candidate ) =>
197+ computeUtilitiesPropertiesLookup . get ( candidate ) ,
198+ )
199+
200+ // For each property, lookup other utilities that also set this property and
201+ // this exact value. If multiple properties are used, use the intersection of
202+ // each property.
203+ //
204+ // E.g.: `margin-top` → `mt-1`, `my-1`, `m-1`
205+ let otherUtilities = candidatePropertiesValues . map ( ( propertyValues ) => {
206+ let result : Set < string > | null = null
207+ for ( let [ property , values ] of propertyValues ) {
208+ for ( let value of values ) {
209+ let otherUtilities = staticUtilities . get ( property ) . get ( value )
210+
211+ if ( result === null ) result = new Set ( otherUtilities )
212+ else result = intersection ( result , otherUtilities )
213+
214+ // The moment no other utilities match, we can stop searching because
215+ // all intersections with an empty set will remain empty.
216+ if ( result ! . size === 0 ) return result !
217+ }
218+ }
219+ return result !
220+ } )
221+
222+ // Link each candidate that could be linked via another utility
223+ // (intersection). This way we can reduce the amount of required combinations.
224+ //
225+ // E.g.: `mt-1` and `mb-1` can be linked via `my-1`.
226+ //
227+ // Candidates that cannot be linked won't be able to be collapsed.
228+ // E.g.: `mt-1` and `text-red-500` cannot be collapsed because there is no 3rd
229+ // utility with overlapping property/value combinations.
230+ let linked = new DefaultMap < number , Set < number > > ( ( key ) => new Set < number > ( [ key ] ) )
231+ let otherUtilitiesArray = Array . from ( otherUtilities )
232+ for ( let i = 0 ; i < otherUtilitiesArray . length ; i ++ ) {
233+ let current = otherUtilitiesArray [ i ]
234+ for ( let j = i + 1 ; j < otherUtilitiesArray . length ; j ++ ) {
235+ let other = otherUtilitiesArray [ j ]
236+
237+ for ( let property of current ) {
238+ if ( other . has ( property ) ) {
239+ linked . get ( i ) . add ( j )
240+ linked . get ( j ) . add ( i )
241+
242+ // The moment we find a link, we can stop comparing and move on to the
243+ // next candidate. This will safe us some time
244+ break
245+ }
246+ }
247+ }
248+ }
249+
250+ // Not a single candidate can be linked to another one, nothing left to do
251+ if ( linked . size === 0 ) return candidates
252+
253+ // Each candidate index will now have a set of other candidate indexes as
254+ // its value. Let's make the lists unique combinations so that we can
255+ // iterate over them.
256+ let uniqueCombinations = new DefaultMap ( ( key : string ) => key . split ( ',' ) . map ( Number ) )
257+ for ( let group of linked . values ( ) ) {
258+ let sorted = Array . from ( group ) . sort ( ( a , b ) => a - b )
259+ uniqueCombinations . get ( sorted . join ( ',' ) )
260+ }
261+
262+ // Let's try to actually collapse them now.
263+ let result = new Set < string > ( candidates )
264+ let drop = new Set < string > ( )
265+
266+ for ( let idxs of uniqueCombinations . values ( ) ) {
267+ for ( let combo of combinations ( idxs ) ) {
268+ if ( combo . some ( ( idx ) => drop . has ( candidates [ idx ] ) ) ) continue // Skip already dropped items
269+
270+ let potentialReplacements = combo . flatMap ( ( idx ) => otherUtilities [ idx ] ) . reduce ( intersection )
271+
272+ let collapsedSignature = computeUtilitySignature . get ( signatureOptions ) . get (
273+ combo
274+ . map ( ( idx ) => candidates [ idx ] )
275+ . sort ( ( a , z ) => a . localeCompare ( z ) ) // Sort to increase cache hits
276+ . join ( ' ' ) ,
277+ )
278+
279+ for ( let replacement of potentialReplacements ) {
280+ let signature = computeUtilitySignature . get ( signatureOptions ) . get ( replacement )
281+ if ( signature !== collapsedSignature ) continue // Not a safe replacement
282+
283+ // We can replace all items in the combo with the replacement
284+ for ( let item of combo ) {
285+ drop . add ( candidates [ item ] )
286+ }
287+
288+ // Use the replacement
289+ result . add ( replacement )
290+ break
291+ }
292+ }
293+ }
294+
295+ for ( let item of drop ) {
296+ result . delete ( item )
297+ }
298+
299+ return Array . from ( result )
300+ }
133301}
134302
135303const canonicalizeCandidateCache = new DefaultMap ( ( options : InternalCanonicalizeOptions ) => {
@@ -1634,3 +1802,51 @@ function optimizeModifier(candidate: Candidate, options: InternalCanonicalizeOpt
16341802
16351803 return candidate
16361804}
1805+
1806+ // Generator that generates all combinations of the given set. Using a generator
1807+ // so we can stop early when we found a suitable combination.
1808+ //
1809+ // NOTE:
1810+ //
1811+ // 1. Yield biggest combinations first
1812+ // 2. Sets of size 1 and 0 are not yielded
1813+ function * combinations < T > ( arr : T [ ] ) : Generator < T [ ] > {
1814+ let n = arr . length
1815+
1816+ for ( let k = n ; k >= 2 ; k -- ) {
1817+ let mask = ( 1n << BigInt ( k ) ) - 1n
1818+ let limit = 1n << BigInt ( n )
1819+
1820+ while ( mask < limit ) {
1821+ let out = new Array < T > ( k )
1822+ let p = 0
1823+ for ( let i = 0 ; i < n ; i ++ ) {
1824+ if ( ( mask >> BigInt ( i ) ) & 1n ) {
1825+ out [ p ++ ] = arr [ i ]
1826+ }
1827+ }
1828+ yield out
1829+
1830+ let carry = mask & - mask
1831+ let ripple = mask + carry
1832+ mask = ( ( ( ripple ^ mask ) >> 2n ) / carry ) | ripple
1833+ }
1834+ }
1835+ }
1836+
1837+ function intersection < T > ( a : Set < T > , b : Set < T > ) : Set < T > {
1838+ // @ts -expect-error Set.prototype.intersection is only available in Node.js v22+
1839+ if ( typeof a . intersection === 'function' ) return a . intersection ( b )
1840+
1841+ // Polyfill for environments that do not support Set.prototype.intersection yet
1842+ if ( a . size === 0 || b . size === 0 ) return new Set < T > ( )
1843+
1844+ let result = new Set < T > ( a )
1845+ for ( let item of b ) {
1846+ if ( ! result . has ( item ) ) {
1847+ result . delete ( item )
1848+ }
1849+ }
1850+
1851+ return result
1852+ }
0 commit comments