@@ -8,46 +8,97 @@ import {
88 ThemeGetter ,
99 ThemeObject ,
1010} from './types'
11+ import { createLruCache } from './lru-cache'
1112
1213export interface ClassPartObject {
1314 nextPart : Map < string , ClassPartObject >
1415 validators : ClassValidatorObject [ ]
15- classGroupId ? : AnyClassGroupIds
16+ classGroupId : AnyClassGroupIds | undefined // Always define optional props for consistent shape
1617}
1718
1819interface ClassValidatorObject {
1920 classGroupId : AnyClassGroupIds
2021 validator : ClassValidator
2122}
2223
24+ // Factory function ensures consistent object shapes
25+ const createClassValidatorObject = (
26+ classGroupId : AnyClassGroupIds ,
27+ validator : ClassValidator ,
28+ ) : ClassValidatorObject => ( {
29+ classGroupId,
30+ validator,
31+ } )
32+
33+ // Factory ensures consistent ClassPartObject shape
34+ const createClassPartObject = (
35+ nextPart : Map < string , ClassPartObject > = new Map ( ) ,
36+ validators : ClassValidatorObject [ ] = [ ] ,
37+ classGroupId ?: AnyClassGroupIds ,
38+ ) : ClassPartObject => ( {
39+ nextPart,
40+ validators,
41+ classGroupId,
42+ } )
43+
2344const CLASS_PART_SEPARATOR = '-'
45+ const EMPTY_VALIDATORS : readonly ClassValidatorObject [ ] = [ ]
46+ const EMPTY_CONFLICTS : readonly AnyClassGroupIds [ ] = [ ]
47+ const ARBITRARY_PROPERTY_PREFIX = 'arbitrary..'
48+ const ARBITRARY_PROPERTY_REGEX = / ^ \[ ( .+ ) \] $ /
2449
2550export const createClassGroupUtils = ( config : AnyConfig ) => {
2651 const classMap = createClassMap ( config )
2752 const { conflictingClassGroups, conflictingClassGroupModifiers } = config
2853
54+ // Use the existing LRU cache implementation
55+ const classGroupCache = createLruCache < string , AnyClassGroupIds | undefined > ( config . cacheSize )
56+
2957 const getClassGroupId = ( className : string ) => {
30- const classParts = className . split ( CLASS_PART_SEPARATOR )
58+ // Check cache first for efficiency
59+ const cachedResult = classGroupCache . get ( className )
60+ if ( cachedResult !== undefined ) {
61+ return cachedResult
62+ }
3163
32- // Classes like `-inset-1` produce an empty string as first classPart. We assume that classes for negative values are used correctly and remove it from classParts.
33- if ( classParts [ 0 ] === '' && classParts . length !== 1 ) {
34- classParts . shift ( )
64+ let result : AnyClassGroupIds | undefined
65+
66+ if ( className . startsWith ( '[' ) ) {
67+ result = getGroupIdForArbitraryProperty ( className )
68+ } else {
69+ const classParts = className . split ( CLASS_PART_SEPARATOR )
70+ if ( classParts [ 0 ] === '' && classParts . length > 1 ) {
71+ classParts . shift ( )
72+ }
73+ result = getGroupRecursive ( classParts , classMap )
3574 }
3675
37- return getGroupRecursive ( classParts , classMap ) || getGroupIdForArbitraryProperty ( className )
76+ // Cache result to avoid repeated computation
77+ classGroupCache . set ( className , result )
78+ return result
3879 }
3980
4081 const getConflictingClassGroupIds = (
4182 classGroupId : AnyClassGroupIds ,
4283 hasPostfixModifier : boolean ,
43- ) => {
44- const conflicts = conflictingClassGroups [ classGroupId ] || [ ]
45-
46- if ( hasPostfixModifier && conflictingClassGroupModifiers [ classGroupId ] ) {
47- return [ ...conflicts , ...conflictingClassGroupModifiers [ classGroupId ] ! ]
84+ ) : readonly AnyClassGroupIds [ ] => {
85+ const conflicts = conflictingClassGroups [ classGroupId ]
86+ if ( ! hasPostfixModifier || ! conflictingClassGroupModifiers [ classGroupId ] ) {
87+ return conflicts || EMPTY_CONFLICTS
4888 }
4989
50- return conflicts
90+ const modifierConflicts = conflictingClassGroupModifiers [ classGroupId ] !
91+ if ( ! conflicts ) return modifierConflicts
92+
93+ // Pre-allocate for better V8 optimization
94+ const result = new Array ( conflicts . length + modifierConflicts . length )
95+ for ( let i = 0 ; i < conflicts . length ; i ++ ) {
96+ result [ i ] = conflicts [ i ]
97+ }
98+ for ( let i = 0 ; i < modifierConflicts . length ; i ++ ) {
99+ result [ conflicts . length + i ] = modifierConflicts [ i ]
100+ }
101+ return result
51102 }
52103
53104 return {
@@ -60,58 +111,65 @@ const getGroupRecursive = (
60111 classParts : string [ ] ,
61112 classPartObject : ClassPartObject ,
62113) : AnyClassGroupIds | undefined => {
63- if ( classParts . length === 0 ) {
114+ const len = classParts . length
115+ if ( len === 0 ) {
64116 return classPartObject . classGroupId
65117 }
66118
67119 const currentClassPart = classParts [ 0 ] !
68120 const nextClassPartObject = classPartObject . nextPart . get ( currentClassPart )
69- const classGroupFromNextClassPart = nextClassPartObject
70- ? getGroupRecursive ( classParts . slice ( 1 ) , nextClassPartObject )
71- : undefined
72121
73- if ( classGroupFromNextClassPart ) {
74- return classGroupFromNextClassPart
122+ if ( nextClassPartObject ) {
123+ const result = getGroupRecursive ( classParts . slice ( 1 ) , nextClassPartObject )
124+ if ( result ) return result
75125 }
76126
77- if ( classPartObject . validators . length === 0 ) {
127+ const validators = classPartObject . validators
128+ if ( validators . length === 0 ) {
78129 return undefined
79130 }
80131
81132 const classRest = classParts . join ( CLASS_PART_SEPARATOR )
133+ const len2 = validators . length
82134
83- return classPartObject . validators . find ( ( { validator } ) => validator ( classRest ) ) ?. classGroupId
135+ for ( let i = 0 ; i < len2 ; i ++ ) {
136+ const validatorObj = validators [ i ] !
137+ if ( validatorObj . validator ( classRest ) ) {
138+ return validatorObj . classGroupId
139+ }
140+ }
141+
142+ return undefined
84143}
85144
86- const arbitraryPropertyRegex = / ^ \[ ( .+ ) \] $ /
145+ const getGroupIdForArbitraryProperty = ( className : string ) : AnyClassGroupIds | undefined => {
146+ const match = ARBITRARY_PROPERTY_REGEX . exec ( className )
147+ if ( ! match ?. [ 1 ] ) return undefined
87148
88- const getGroupIdForArbitraryProperty = ( className : string ) => {
89- if ( arbitraryPropertyRegex . test ( className ) ) {
90- const arbitraryPropertyClassName = arbitraryPropertyRegex . exec ( className ) ! [ 1 ]
91- const property = arbitraryPropertyClassName ?. substring (
92- 0 ,
93- arbitraryPropertyClassName . indexOf ( ':' ) ,
94- )
149+ const colonIndex = match [ 1 ] . indexOf ( ':' )
150+ if ( colonIndex === - 1 ) return undefined
95151
96- if ( property ) {
97- // I use two dots here because one dot is used as prefix for class groups in plugins
98- return 'arbitrary..' + property
99- }
100- }
152+ const property = match [ 1 ] . slice ( 0 , colonIndex )
153+ return property ? ARBITRARY_PROPERTY_PREFIX + property : undefined
101154}
102155
103- /**
104- * Exported for testing only
105- */
106156export const createClassMap = ( config : Config < AnyClassGroupIds , AnyThemeGroupIds > ) => {
107157 const { theme, classGroups } = config
108- const classMap : ClassPartObject = {
109- nextPart : new Map < string , ClassPartObject > ( ) ,
110- validators : [ ] ,
111- }
158+ return processClassGroups ( classGroups , theme )
159+ }
160+
161+ // Split into separate functions to maintain monomorphic call sites
162+ const processClassGroups = (
163+ classGroups : Record < AnyClassGroupIds , ClassGroup < AnyThemeGroupIds > > ,
164+ theme : ThemeObject < AnyThemeGroupIds > ,
165+ ) : ClassPartObject => {
166+ const classMap = createClassPartObject ( )
112167
113168 for ( const classGroupId in classGroups ) {
114- processClassesRecursively ( classGroups [ classGroupId ] ! , classMap , classGroupId , theme )
169+ const group = classGroups [ classGroupId ]
170+ if ( group ) {
171+ processClassesRecursively ( group , classMap , classGroupId , theme )
172+ }
115173 }
116174
117175 return classMap
@@ -123,60 +181,103 @@ const processClassesRecursively = (
123181 classGroupId : AnyClassGroupIds ,
124182 theme : ThemeObject < AnyThemeGroupIds > ,
125183) => {
126- classGroup . forEach ( ( classDefinition ) => {
127- if ( typeof classDefinition === 'string' ) {
128- const classPartObjectToEdit =
129- classDefinition === '' ? classPartObject : getPart ( classPartObject , classDefinition )
130- classPartObjectToEdit . classGroupId = classGroupId
131- return
132- }
184+ const len = classGroup . length
185+ for ( let i = 0 ; i < len ; i ++ ) {
186+ const classDefinition = classGroup [ i ] !
187+ processClassDefinition ( classDefinition , classPartObject , classGroupId , theme )
188+ }
189+ }
133190
134- if ( typeof classDefinition === 'function' ) {
135- if ( isThemeGetter ( classDefinition ) ) {
136- processClassesRecursively (
137- classDefinition ( theme ) ,
138- classPartObject ,
139- classGroupId ,
140- theme ,
141- )
142- return
143- }
191+ // Split into separate functions for each type to maintain monomorphic call sites
192+ const processClassDefinition = (
193+ classDefinition : ClassGroup < AnyThemeGroupIds > [ number ] ,
194+ classPartObject : ClassPartObject ,
195+ classGroupId : AnyClassGroupIds ,
196+ theme : ThemeObject < AnyThemeGroupIds > ,
197+ ) => {
198+ if ( typeof classDefinition === 'string' ) {
199+ processStringDefinition ( classDefinition , classPartObject , classGroupId )
200+ return
201+ }
144202
145- classPartObject . validators . push ( {
146- validator : classDefinition ,
147- classGroupId ,
148- } )
203+ if ( typeof classDefinition === 'function' ) {
204+ processFunctionDefinition ( classDefinition , classPartObject , classGroupId , theme )
205+ return
206+ }
149207
150- return
151- }
208+ if ( classDefinition ) {
209+ processObjectDefinition (
210+ classDefinition as Record < string , ClassGroup < AnyThemeGroupIds > > ,
211+ classPartObject ,
212+ classGroupId ,
213+ theme ,
214+ )
215+ }
216+ }
217+
218+ const processStringDefinition = (
219+ classDefinition : string ,
220+ classPartObject : ClassPartObject ,
221+ classGroupId : AnyClassGroupIds ,
222+ ) => {
223+ const classPartObjectToEdit =
224+ classDefinition === '' ? classPartObject : getPart ( classPartObject , classDefinition )
225+ classPartObjectToEdit . classGroupId = classGroupId
226+ }
227+
228+ const processFunctionDefinition = (
229+ classDefinition : Function ,
230+ classPartObject : ClassPartObject ,
231+ classGroupId : AnyClassGroupIds ,
232+ theme : ThemeObject < AnyThemeGroupIds > ,
233+ ) => {
234+ if ( isThemeGetter ( classDefinition ) ) {
235+ processClassesRecursively ( classDefinition ( theme ) , classPartObject , classGroupId , theme )
236+ return
237+ }
152238
153- Object . entries ( classDefinition ) . forEach ( ( [ key , classGroup ] ) => {
154- processClassesRecursively (
155- classGroup ,
156- getPart ( classPartObject , key ) ,
157- classGroupId ,
158- theme ,
159- )
160- } )
161- } )
239+ if ( classPartObject . validators === EMPTY_VALIDATORS ) {
240+ classPartObject . validators = [ ]
241+ }
242+ classPartObject . validators . push (
243+ createClassValidatorObject ( classGroupId , classDefinition as ClassValidator ) ,
244+ )
245+ }
246+
247+ const processObjectDefinition = (
248+ classDefinition : Record < string , ClassGroup < AnyThemeGroupIds > > ,
249+ classPartObject : ClassPartObject ,
250+ classGroupId : AnyClassGroupIds ,
251+ theme : ThemeObject < AnyThemeGroupIds > ,
252+ ) => {
253+ const entries = Object . entries ( classDefinition )
254+ const len = entries . length
255+ for ( let i = 0 ; i < len ; i ++ ) {
256+ const [ key , value ] = entries [ i ] !
257+ processClassesRecursively ( value , getPart ( classPartObject , key ) , classGroupId , theme )
258+ }
162259}
163260
164- const getPart = ( classPartObject : ClassPartObject , path : string ) => {
165- let currentClassPartObject = classPartObject
261+ const getPart = ( classPartObject : ClassPartObject , path : string ) : ClassPartObject => {
262+ let current = classPartObject
263+ const parts = path . split ( CLASS_PART_SEPARATOR )
264+ const len = parts . length
166265
167- path . split ( CLASS_PART_SEPARATOR ) . forEach ( ( pathPart ) => {
168- if ( ! currentClassPartObject . nextPart . has ( pathPart ) ) {
169- currentClassPartObject . nextPart . set ( pathPart , {
170- nextPart : new Map ( ) ,
171- validators : [ ] ,
172- } )
173- }
266+ for ( let i = 0 ; i < len ; i ++ ) {
267+ const part = parts [ i ]
268+ if ( ! part ) continue
174269
175- currentClassPartObject = currentClassPartObject . nextPart . get ( pathPart ) !
176- } )
270+ let next = current . nextPart . get ( part )
271+ if ( ! next ) {
272+ next = createClassPartObject ( )
273+ current . nextPart . set ( part , next )
274+ }
275+ current = next
276+ }
177277
178- return currentClassPartObject
278+ return current
179279}
180280
181- const isThemeGetter = ( func : ClassValidator | ThemeGetter ) : func is ThemeGetter =>
182- ( func as ThemeGetter ) . isThemeGetter
281+ // Type guard maintains monomorphic check
282+ const isThemeGetter = ( func : Function ) : func is ThemeGetter =>
283+ 'isThemeGetter' in func && ( func as ThemeGetter ) . isThemeGetter === true
0 commit comments