Skip to content

Commit 4129ea4

Browse files
author
Evan Jacobs
committed
vibe optimization
1 parent fd635e1 commit 4129ea4

9 files changed

Lines changed: 335 additions & 182 deletions

src/lib/class-group-utils.ts

Lines changed: 188 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -8,46 +8,97 @@ import {
88
ThemeGetter,
99
ThemeObject,
1010
} from './types'
11+
import { createLruCache } from './lru-cache'
1112

1213
export 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

1819
interface 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+
2344
const 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

2550
export 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-
*/
106156
export 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

src/lib/default-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export const getDefaultConfig = () => {
174174
const scaleTranslate = () => [isFraction, 'full', ...scaleUnambiguousSpacing()] as const
175175

176176
return {
177-
cacheSize: 500,
177+
cacheSize: 20000,
178178
theme: {
179179
animate: ['spin', 'ping', 'pulse', 'bounce'],
180180
aspect: ['video'],

src/lib/from-theme.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { DefaultThemeGroupIds, NoInfer, ThemeGetter, ThemeObject } from './types'
22

3+
const fallbackThemeArr: ThemeObject<DefaultThemeGroupIds>[DefaultThemeGroupIds] = []
4+
35
export const fromTheme = <
46
AdditionalThemeGroupIds extends string = never,
57
DefaultThemeGroupIdsInner extends string = DefaultThemeGroupIds,
6-
>(key: NoInfer<DefaultThemeGroupIdsInner | AdditionalThemeGroupIds>): ThemeGetter => {
8+
>(
9+
key: NoInfer<DefaultThemeGroupIdsInner | AdditionalThemeGroupIds>,
10+
): ThemeGetter => {
711
const themeGetter = (theme: ThemeObject<DefaultThemeGroupIdsInner | AdditionalThemeGroupIds>) =>
8-
theme[key] || []
12+
theme[key] || fallbackThemeArr
913

1014
themeGetter.isThemeGetter = true as const
1115

0 commit comments

Comments
 (0)