@tamagui/static 1.132.16 → 1.132.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/extractor/createEvaluator.js +13 -1
  2. package/dist/extractor/createEvaluator.js.map +1 -1
  3. package/dist/extractor/createEvaluator.native.js +6 -1
  4. package/dist/extractor/createEvaluator.native.js.map +2 -2
  5. package/dist/extractor/createExtractor.js +60 -114
  6. package/dist/extractor/createExtractor.js.map +1 -1
  7. package/dist/extractor/createExtractor.native.js +70 -123
  8. package/dist/extractor/createExtractor.native.js.map +2 -2
  9. package/dist/extractor/errors.js +22 -0
  10. package/dist/extractor/errors.js.map +6 -0
  11. package/dist/extractor/errors.native.js +119 -0
  12. package/dist/extractor/errors.native.js.map +6 -0
  13. package/dist/extractor/extractMediaStyle.js +1 -1
  14. package/dist/extractor/extractMediaStyle.js.map +1 -1
  15. package/dist/extractor/extractMediaStyle.native.js +1 -1
  16. package/dist/extractor/extractMediaStyle.native.js.map +2 -2
  17. package/dist/extractor/extractToClassNames.js +206 -172
  18. package/dist/extractor/extractToClassNames.js.map +2 -2
  19. package/dist/extractor/extractToClassNames.native.js +212 -188
  20. package/dist/extractor/extractToClassNames.native.js.map +2 -2
  21. package/dist/extractor/extractToNative.js +47 -78
  22. package/dist/extractor/extractToNative.js.map +1 -1
  23. package/dist/extractor/extractToNative.native.js +23 -39
  24. package/dist/extractor/extractToNative.native.js.map +2 -2
  25. package/dist/extractor/normalizeTernaries.js +5 -3
  26. package/dist/extractor/normalizeTernaries.js.map +1 -1
  27. package/dist/extractor/normalizeTernaries.native.js +5 -3
  28. package/dist/extractor/normalizeTernaries.native.js.map +2 -2
  29. package/dist/extractor/propsToFontFamilyCache.js +7 -8
  30. package/dist/extractor/propsToFontFamilyCache.js.map +1 -1
  31. package/dist/extractor/propsToFontFamilyCache.native.js +9 -10
  32. package/dist/extractor/propsToFontFamilyCache.native.js.map +2 -2
  33. package/dist/registerRequire.js +1 -1
  34. package/dist/registerRequire.js.map +1 -1
  35. package/dist/registerRequire.native.js +1 -1
  36. package/dist/registerRequire.native.js.map +1 -1
  37. package/dist/types.native.js.map +1 -1
  38. package/package.json +15 -15
  39. package/src/extractor/createEvaluator.ts +26 -1
  40. package/src/extractor/createExtractor.ts +108 -194
  41. package/src/extractor/errors.ts +1 -0
  42. package/src/extractor/extractMediaStyle.ts +1 -1
  43. package/src/extractor/extractToClassNames.ts +362 -266
  44. package/src/extractor/extractToNative.ts +68 -111
  45. package/src/extractor/normalizeTernaries.ts +10 -3
  46. package/src/extractor/propsToFontFamilyCache.ts +5 -5
  47. package/src/registerRequire.ts +1 -1
  48. package/src/types.ts +10 -13
  49. package/types/extractor/createEvaluator.d.ts.map +1 -1
  50. package/types/extractor/createExtractor.d.ts.map +1 -1
  51. package/types/extractor/errors.d.ts +3 -0
  52. package/types/extractor/errors.d.ts.map +1 -0
  53. package/types/extractor/extractToClassNames.d.ts.map +1 -1
  54. package/types/extractor/extractToNative.d.ts.map +1 -1
  55. package/types/extractor/normalizeTernaries.d.ts.map +1 -1
  56. package/types/extractor/propsToFontFamilyCache.d.ts +2 -2
  57. package/types/extractor/propsToFontFamilyCache.d.ts.map +1 -1
  58. package/types/types.d.ts +9 -10
  59. package/types/types.d.ts.map +1 -1
  60. package/dist/extractor/buildClassName.js +0 -72
  61. package/dist/extractor/buildClassName.js.map +0 -6
  62. package/dist/extractor/buildClassName.native.js +0 -67
  63. package/dist/extractor/buildClassName.native.js.map +0 -6
  64. package/dist/extractor/ensureImportingConcat.js +0 -50
  65. package/dist/extractor/ensureImportingConcat.js.map +0 -6
  66. package/dist/extractor/ensureImportingConcat.native.js +0 -49
  67. package/dist/extractor/ensureImportingConcat.native.js.map +0 -6
  68. package/dist/extractor/hoistClassNames.js +0 -63
  69. package/dist/extractor/hoistClassNames.js.map +0 -6
  70. package/dist/extractor/hoistClassNames.native.js +0 -66
  71. package/dist/extractor/hoistClassNames.native.js.map +0 -6
  72. package/src/extractor/buildClassName.ts +0 -76
  73. package/src/extractor/ensureImportingConcat.ts +0 -36
  74. package/src/extractor/hoistClassNames.ts +0 -52
  75. package/types/extractor/buildClassName.d.ts +0 -7
  76. package/types/extractor/buildClassName.d.ts.map +0 -1
  77. package/types/extractor/ensureImportingConcat.d.ts +0 -4
  78. package/types/extractor/ensureImportingConcat.d.ts.map +0 -1
  79. package/types/extractor/hoistClassNames.d.ts +0 -6
  80. package/types/extractor/hoistClassNames.d.ts.map +0 -1
@@ -1,30 +1,22 @@
1
- import * as path from 'node:path'
2
- import * as util from 'node:util'
3
-
4
1
  import generate from '@babel/generator'
2
+ import type { NodePath } from '@babel/traverse'
5
3
  import * as t from '@babel/types'
6
- import * as helpers from '@tamagui/helpers'
7
- import type { ViewStyle } from 'react-native'
8
-
4
+ import { mergeProps, StyleObjectIdentifier, StyleObjectRules } from '@tamagui/web'
5
+ import * as path from 'node:path'
6
+ import * as util from 'node:util'
9
7
  import { requireTamaguiCore } from '../helpers/requireTamaguiCore'
10
- import type { ClassNameObject, StyleObject, TamaguiOptions, Ternary } from '../types'
8
+ import type { StyleObject, TamaguiOptions, Ternary } from '../types'
11
9
  import { babelParse } from './babelParse'
12
- import { buildClassName } from './buildClassName'
13
10
  import type { Extractor } from './createExtractor'
14
11
  import { createLogger } from './createLogger'
15
- import { ensureImportingConcat } from './ensureImportingConcat'
16
- import { isSimpleSpread } from './extractHelpers'
17
12
  import { extractMediaStyle } from './extractMediaStyle'
18
- import { hoistClassNames } from './hoistClassNames'
19
- import { getFontFamilyClassNameFromProps } from './propsToFontFamilyCache'
13
+ import { normalizeTernaries } from './normalizeTernaries'
14
+ import {
15
+ forwardFontFamilyName,
16
+ getFontFamilyNameFromProps,
17
+ } from './propsToFontFamilyCache'
20
18
  import { timer } from './timer'
21
-
22
- const mergeStyleGroups = {
23
- shadowOpacity: true,
24
- shadowRadius: true,
25
- shadowColor: true,
26
- shadowOffset: true,
27
- }
19
+ import { BailOptimizationError } from './errors'
28
20
 
29
21
  export type ExtractedResponse = {
30
22
  js: string | Buffer
@@ -42,6 +34,12 @@ export type ExtractToClassNamesProps = {
42
34
  shouldPrintDebug: boolean | 'verbose'
43
35
  }
44
36
 
37
+ // we only expand into ternaries or plain attr, all style is turned into a always-true ternary
38
+ // this lets us more easily combine everything easily
39
+ // all ternaries in this array ONLY have consequent, they are normalized
40
+ const remove = () => {} // we dont remove after this step
41
+ const spaceString = t.stringLiteral(' ')
42
+
45
43
  export async function extractToClassNames({
46
44
  extractor,
47
45
  source,
@@ -50,7 +48,7 @@ export async function extractToClassNames({
50
48
  shouldPrintDebug,
51
49
  }: ExtractToClassNamesProps): Promise<ExtractedResponse | null> {
52
50
  const tm = timer()
53
- const { getCSSStylesAtomic } = requireTamaguiCore('web')
51
+ const { getCSSStylesAtomic, createMediaStyle } = requireTamaguiCore('web')
54
52
 
55
53
  if (sourcePath.includes('node_modules')) {
56
54
  return null
@@ -93,9 +91,7 @@ export async function extractToClassNames({
93
91
  tm.mark(`babel-parse`, shouldPrintDebug === 'verbose')
94
92
 
95
93
  const cssMap = new Map<string, { css: string; commentTexts: string[] }>()
96
- const existingHoists: { [key: string]: t.Identifier } = {}
97
-
98
- let hasFlattened = false
94
+ const tamaguiConfig = extractor.getTamagui()!
99
95
 
100
96
  const res = await extractor.parse(ast, {
101
97
  shouldPrintDebug,
@@ -103,7 +99,7 @@ export async function extractToClassNames({
103
99
  platform: 'web',
104
100
  sourcePath,
105
101
  extractStyledDefinitions: true,
106
- onStyleRule(identifier, rules) {
102
+ onStyledDefinitionRule(identifier, rules) {
107
103
  const css = rules.join(';')
108
104
  if (shouldPrintDebug) {
109
105
  console.info(`adding styled() rule: .${identifier} ${css}`)
@@ -111,7 +107,6 @@ export async function extractToClassNames({
111
107
  cssMap.set(`.${identifier}`, { css, commentTexts: [] })
112
108
  },
113
109
  getFlattenedNode: ({ tag }) => {
114
- hasFlattened = true
115
110
  return tag
116
111
  },
117
112
  onExtractTag: ({
@@ -123,300 +118,348 @@ export async function extractToClassNames({
123
118
  originalNodeName,
124
119
  filePath,
125
120
  lineNumbers,
126
- programPath,
127
- isFlattened,
128
121
  staticConfig,
129
122
  }) => {
130
123
  // bail out of views that don't accept className (falls back to runtime + style={})
131
124
  if (staticConfig.acceptsClassName === false) {
132
- if (shouldPrintDebug) {
133
- console.info(`bail, acceptsClassName is false`)
134
- }
135
- return
125
+ throw new BailOptimizationError()
136
126
  }
137
127
 
138
- // reset hasFlattened
139
- const didFlattenThisTag = hasFlattened
140
- hasFlattened = false
128
+ // re-worked how we do this
129
+ // merging ternaries on top of base styles is not simple, because we need to ensure the final
130
+ // className has no duplicate style props and selector order is preserved
131
+ // before we tried to be smart and build a big binary expression
132
+ // instead, what we'll do now is pre-calculate the entire className for every possible path
133
+ // for super complex components that means we *will* output a lot of bigger classNames
134
+ // but its so much simpler than trying to implement a multi-stage solver here
135
+ // and in the end its just strings that gzip very well
136
+ // its also much easier to intuit/debug for end users and ourselves
137
+
138
+ // example:
139
+ // a ? 'a' : 'b'
140
+ // b ? 'c' : 'd'
141
+ // we want:
142
+ // a && b ? 'a c' : ''
143
+ // !a && b ? 'b c' : ''
144
+ // a && !b ? 'a d' : ''
145
+ // !a && !b ? 'b d' : ''
146
+
147
+ // we also simplified the compiler to only handle views that can be fully flattened
148
+ // this means we don't need to account for strange in-between spreads, so we can merge things
149
+ // fairly simply. first, we just merge forward all the non-ternary styles into ternaries.
150
+
151
+ // save for the end
152
+ const finalAttrs: t.JSXAttribute[] = []
153
+
154
+ let mergeForwardBaseStyle: Object | null = null
155
+ let attrClassName: t.Expression | null = null
156
+ let baseFontFamily = ''
157
+ let mediaStylesSeen = 1
141
158
 
142
- let finalClassNames: ClassNameObject[] = []
143
- const finalAttrs: (t.JSXAttribute | t.JSXSpreadAttribute)[] = []
144
- let finalStyles: StyleObject[] = []
159
+ const comment = util.format(
160
+ '/* %s:%s (%s) */',
161
+ filePath,
162
+ lineNumbers,
163
+ originalNodeName
164
+ )
145
165
 
146
- let viewStyles = {}
147
- for (const attr of attrs) {
148
- if (attr.type === 'style') {
149
- viewStyles = {
150
- ...viewStyles,
151
- ...attr.value,
152
- }
166
+ function addStyle(style: StyleObject) {
167
+ const identifier = style[StyleObjectIdentifier]
168
+ const rules = style[StyleObjectRules]
169
+ const selector = `.${identifier}`
170
+ if (cssMap.has(selector)) {
171
+ const val = cssMap.get(selector)!
172
+ val.commentTexts.push(comment)
173
+ } else if (rules.length) {
174
+ cssMap.set(selector, {
175
+ css: rules.join('\n'),
176
+ commentTexts: [comment],
177
+ })
153
178
  }
179
+ return identifier
154
180
  }
155
181
 
156
- const ensureNeededPrevStyle = (style: ViewStyle) => {
157
- // ensure all group keys are merged
158
- const keys = Object.keys(style)
159
- if (!keys.some((key) => mergeStyleGroups[key])) {
160
- return style
161
- }
162
- for (const k in mergeStyleGroups) {
163
- if (k in viewStyles) {
164
- style[k] = style[k] ?? viewStyles[k]
182
+ function addStyles(style: Object) {
183
+ const cssStyles = getCSSStylesAtomic(style as any)
184
+ const classNames: string[] = []
185
+
186
+ for (const style of cssStyles) {
187
+ const mediaName = style[0].slice(1)
188
+ if (tamaguiConfig.media[mediaName]) {
189
+ const mediaStyle = createMediaStyle(
190
+ style,
191
+ mediaName,
192
+ extractor.getTamagui()!.media,
193
+ true,
194
+ false,
195
+ mediaStylesSeen
196
+ )
197
+ const identifier = addStyle(mediaStyle)
198
+ classNames.push(identifier)
199
+ continue
165
200
  }
166
- }
167
- return style
168
- }
169
201
 
170
- const addStyles = (style: ViewStyle | null): StyleObject[] => {
171
- if (!style) return []
172
- const styleWithPrev = ensureNeededPrevStyle(style)
173
- const res = getCSSStylesAtomic(styleWithPrev as any)
174
- if (res.length) {
175
- finalStyles = [...finalStyles, ...res]
202
+ const identifier = addStyle(style)
203
+ classNames.push(identifier)
176
204
  }
177
- return res
205
+
206
+ return classNames
178
207
  }
179
208
 
180
- // 1 to start above any :hover styles
181
- let lastMediaImportance = 1
182
- for (const attr of attrs) {
183
- switch (attr.type) {
184
- case 'style': {
185
- if (!isFlattened) {
186
- const styles = getCSSStylesAtomic(attr.value as any)
187
-
188
- finalStyles = [...finalStyles, ...styles]
189
-
190
- for (const style of styles) {
191
- // leave them as attributes
192
- const prop = style[helpers.StyleObjectPseudo]
193
- ? `${style[helpers.StyleObjectProperty]}-${
194
- style[helpers.StyleObjectPseudo]
195
- }`
196
- : style[helpers.StyleObjectProperty]
197
- finalAttrs.push(
198
- t.jsxAttribute(
199
- t.jsxIdentifier(prop),
200
- t.stringLiteral(style[helpers.StyleObjectIdentifier])
201
- )
202
- )
203
- }
204
- } else {
205
- const styles = addStyles(attr.value)
206
- const newFontFamily = getFontFamilyClassNameFromProps(attr.value) || ''
207
- const newClassNames = helpers.concatClassName(
208
- styles.map((x) => x[helpers.StyleObjectIdentifier]).join(' ') +
209
- newFontFamily
210
- )
211
- const existing = finalClassNames.find(
212
- (x) => x.type == 'StringLiteral'
213
- ) as t.StringLiteral | null
214
-
215
- if (existing) {
216
- let previous = existing.value
217
- // replace existing font_ with new one
218
- if (newFontFamily) {
219
- if (shouldPrintDebug) {
220
- console.info(` newFontFamily: ${newFontFamily}`)
221
- }
222
- previous = previous.replace(/font_[a-z]+/i, '')
223
- }
224
- existing.value = `${previous} ${newClassNames}`
225
- } else {
226
- finalClassNames = [...finalClassNames, t.stringLiteral(newClassNames)]
227
- }
228
- }
209
+ const onlyTernaries: Ternary[] = attrs.flatMap((attr) => {
210
+ if (attr.type === 'attr') {
211
+ const value = attr.value
229
212
 
230
- break
213
+ if (t.isJSXSpreadAttribute(value)) {
214
+ // we only handle flattened stuff now so skip this
215
+ console.error(`Should never happen`)
216
+ return []
231
217
  }
232
- case 'attr': {
233
- const val = attr.value
234
- if (t.isJSXSpreadAttribute(val)) {
235
- if (isSimpleSpread(val)) {
236
- finalClassNames.push(
237
- t.logicalExpression(
238
- '&&',
239
- val.argument,
240
- t.memberExpression(val.argument, t.identifier('className'))
241
- )
242
- )
243
- }
244
- } else if (val.name.name === 'className') {
245
- const value = val.value
246
- if (value) {
247
- try {
248
- const evaluatedValue = attemptEval(value)
249
- finalClassNames.push(t.stringLiteral(evaluatedValue))
250
- } catch (e) {
251
- finalClassNames.push(value['expression'])
252
- }
253
- }
254
- continue
218
+
219
+ if (value.name.name === 'className') {
220
+ let inner: any = value.value
221
+ if (t.isJSXExpressionContainer(inner)) {
222
+ inner = inner.expression
255
223
  }
256
- finalAttrs.push(val)
257
- break
258
- }
259
- case 'ternary': {
260
- const mediaExtraction = extractMediaStyle(
261
- parserProps,
262
- attr.value,
263
- jsxPath,
264
- extractor.getTamagui()!,
265
- sourcePath || '',
266
- lastMediaImportance,
267
- shouldPrintDebug
268
- )
269
- if (shouldPrintDebug) {
270
- if (mediaExtraction) {
271
- console.info(
272
- 'ternary (mediaStyles)',
273
- mediaExtraction.ternaryWithoutMedia?.inlineMediaQuery ?? '',
274
- mediaExtraction.mediaStyles
275
- .map((x) => x[helpers.StyleObjectIdentifier])
276
- .join('.')
277
- )
224
+ try {
225
+ const evaluatedValue = inner ? attemptEval(inner) : null
226
+ if (typeof evaluatedValue === 'string') {
227
+ attrClassName = t.stringLiteral(evaluatedValue)
278
228
  }
279
- }
280
- if (!mediaExtraction) {
281
- if (shouldPrintDebug) {
282
- if (mediaExtraction) {
283
- console.info('add ternary')
284
- }
229
+ } catch (e) {
230
+ if (inner) {
231
+ attrClassName ||= inner
285
232
  }
286
- addTernaryStyle(
287
- attr.value,
288
- addStyles(attr.value.consequent),
289
- addStyles(attr.value.alternate)
290
- )
291
- continue
292
233
  }
293
- lastMediaImportance++
234
+ return []
235
+ }
236
+
237
+ finalAttrs.push(value)
238
+ return []
239
+ }
240
+
241
+ if (attr.type === 'style') {
242
+ mergeForwardBaseStyle = mergeProps(mergeForwardBaseStyle || {}, attr.value)
243
+ baseFontFamily = getFontFamilyNameFromProps(attr.value) || ''
244
+ return []
245
+ }
246
+
247
+ let ternary = attr.value
248
+
249
+ if (ternary.inlineMediaQuery) {
250
+ const mediaExtraction = extractMediaStyle(
251
+ parserProps,
252
+ attr.value,
253
+ jsxPath,
254
+ extractor.getTamagui()!,
255
+ sourcePath || '',
256
+ mediaStylesSeen++,
257
+ shouldPrintDebug
258
+ )
259
+
260
+ if (mediaExtraction) {
294
261
  if (mediaExtraction.mediaStyles) {
295
- finalStyles = [...finalStyles, ...mediaExtraction.mediaStyles]
262
+ mergeForwardBaseStyle = mergeProps(mergeForwardBaseStyle || {}, {
263
+ [`$${ternary.inlineMediaQuery}`]: attr.value.consequent!,
264
+ })
296
265
  }
297
266
  if (mediaExtraction.ternaryWithoutMedia) {
298
- addTernaryStyle(
299
- mediaExtraction.ternaryWithoutMedia,
300
- mediaExtraction.mediaStyles,
301
- []
302
- )
267
+ ternary = mediaExtraction.ternaryWithoutMedia
303
268
  } else {
304
- finalClassNames = [
305
- ...finalClassNames,
306
- ...mediaExtraction.mediaStyles.map((x) =>
307
- t.stringLiteral(x[helpers.StyleObjectIdentifier])
308
- ),
309
- ]
269
+ return []
310
270
  }
311
- break
312
271
  }
313
272
  }
314
- }
315
273
 
316
- function addTernaryStyle(ternary: Ternary, a: StyleObject[], b: StyleObject[]) {
317
- const cCN = a.map((x) => x[helpers.StyleObjectIdentifier]).join(' ')
318
- const aCN = b.map((x) => x[helpers.StyleObjectIdentifier]).join(' ')
274
+ const mergedAlternate = mergeProps(
275
+ mergeForwardBaseStyle || {},
276
+ ternary.alternate || {}
277
+ )
278
+ const mergedConsequent = mergeProps(
279
+ mergeForwardBaseStyle || {},
280
+ ternary.consequent || {}
281
+ )
319
282
 
320
- if (a.length && b.length) {
321
- finalClassNames.push(
322
- t.conditionalExpression(
323
- ternary.test,
324
- t.stringLiteral(cCN),
325
- t.stringLiteral(aCN)
326
- )
327
- )
328
- } else {
329
- finalClassNames.push(
330
- t.conditionalExpression(
331
- ternary.test,
332
- t.stringLiteral(' ' + cCN),
333
- t.stringLiteral(' ' + aCN)
334
- )
335
- )
283
+ forwardFontFamilyName(ternary.alternate, mergedAlternate, baseFontFamily)
284
+ forwardFontFamilyName(ternary.consequent, mergedConsequent, baseFontFamily)
285
+
286
+ // merge the base style forward into both sides
287
+ return {
288
+ ...ternary,
289
+ alternate: mergedAlternate,
290
+ consequent: mergedConsequent,
336
291
  }
337
- }
292
+ })
293
+
294
+ const hasTernaries = Boolean(onlyTernaries.length)
295
+
296
+ const baseClassNames = mergeForwardBaseStyle
297
+ ? addStyles(mergeForwardBaseStyle)
298
+ : null
338
299
 
339
- if (shouldPrintDebug === 'verbose') {
340
- console.info(' finalClassNames AST\n', JSON.stringify(finalClassNames, null, 2))
300
+ let baseClassNameStr =
301
+ hasTernaries || !baseClassNames ? '' : baseClassNames.join(' ')
302
+
303
+ if (!hasTernaries && baseFontFamily) {
304
+ baseClassNameStr = `font_${baseFontFamily}${baseClassNameStr ? ` ${baseClassNameStr}` : ''}`
341
305
  }
342
306
 
343
- node.attributes = finalAttrs
307
+ let base = staticConfig.componentName
308
+ ? t.stringLiteral(
309
+ `is_${staticConfig.componentName}${baseClassNameStr ? ` ${baseClassNameStr}` : ''}`
310
+ )
311
+ : t.stringLiteral(baseClassNameStr || '')
344
312
 
345
- if (finalClassNames.length) {
346
- const extraClassNames = (() => {
347
- let value = ''
348
- if (!isFlattened) {
349
- return value
350
- }
313
+ attrClassName = attrClassName as t.Expression | null // actual typescript bug, flatMap doesn't update from never
351
314
 
352
- // helper to see how many get flattened
353
- if (process.env.TAMAGUI_DEBUG_OPTIMIZATIONS) {
354
- value += `is_tamagui_flattened`
315
+ const baseClassNameExpression: t.Expression = (() => {
316
+ if (attrClassName) {
317
+ if (t.isStringLiteral(attrClassName)) {
318
+ return t.stringLiteral(
319
+ base.value ? `${base.value} ${attrClassName.value}` : attrClassName.value
320
+ )
321
+ } else {
322
+ // space after to ensure its a string and its spaced
323
+ return t.conditionalExpression(
324
+ attrClassName,
325
+ t.binaryExpression('+', attrClassName, spaceString),
326
+ base
327
+ )
355
328
  }
329
+ }
330
+ return base
331
+ })()
356
332
 
357
- // add is_Component className
358
- if (staticConfig.componentName) {
359
- value += ` is_${staticConfig.componentName}`
360
- }
333
+ const expandedTernaries: Ternary[] = []
361
334
 
362
- return value
363
- })()
335
+ if (onlyTernaries.length) {
336
+ // normalize tests to reduce duplicates
337
+ const normalizedTernaries = normalizeTernaries(onlyTernaries)
364
338
 
365
- // inserts the _cn variable and uses it for className
366
- const names = buildClassName(finalClassNames, extraClassNames)
339
+ for (const ternary of normalizedTernaries) {
340
+ if (!expandedTernaries.length) {
341
+ expandTernary(ternary)
342
+ continue
343
+ }
344
+ for (const prev of [...expandedTernaries]) {
345
+ expandTernary(ternary, prev)
346
+ }
347
+ }
348
+ }
367
349
 
368
- const nameExpr = names ? hoistClassNames(jsxPath, existingHoists, names) : null
369
- let expr = nameExpr
350
+ function expandTernary(ternary: Ternary, prev?: Ternary) {
351
+ // need to diverge into two (or four if alternate)
352
+ if (ternary.consequent && Object.keys(ternary.consequent).length) {
353
+ const fontFamily = getFontFamilyNameFromProps(ternary.consequent)
354
+
355
+ expandedTernaries.push({
356
+ fontFamily,
357
+ // prevTest && test: merge consequent
358
+ test: prev
359
+ ? t.logicalExpression('&&', prev.test, ternary.test)
360
+ : ternary.test,
361
+ consequent: prev
362
+ ? mergeProps(prev.consequent!, ternary.consequent)
363
+ : ternary.consequent,
364
+ remove,
365
+ alternate: null,
366
+ })
370
367
 
371
- // if has some spreads, use concat helper
372
- if (nameExpr && !t.isIdentifier(nameExpr)) {
373
- if (!didFlattenThisTag) {
374
- // not flat
375
- } else {
376
- ensureImportingConcat(programPath)
377
- const simpleSpreads = attrs.filter((x) => {
378
- return (
379
- x.type === 'attr' &&
380
- t.isJSXSpreadAttribute(x.value) &&
381
- isSimpleSpread(x.value)
382
- )
368
+ if (prev) {
369
+ expandedTernaries.push({
370
+ fontFamily,
371
+ // !prevTest && test: just consequent
372
+ test: t.logicalExpression(
373
+ '&&',
374
+ t.unaryExpression('!', prev.test),
375
+ ternary.test
376
+ ),
377
+ consequent: ternary.consequent,
378
+ alternate: null,
379
+ remove,
383
380
  })
384
- expr = t.callExpression(t.identifier('concatClassName'), [
385
- expr,
386
- ...simpleSpreads.map((val) => val.value['argument']),
387
- ])
388
381
  }
389
382
  }
390
383
 
391
- node.attributes.push(
392
- t.jsxAttribute(t.jsxIdentifier('className'), t.jsxExpressionContainer(expr))
393
- )
384
+ if (ternary.alternate && Object.keys(ternary.alternate).length) {
385
+ const fontFamily = getFontFamilyNameFromProps(ternary.alternate)
386
+ const negated = t.unaryExpression('!', ternary.test)
387
+ expandedTernaries.push({
388
+ fontFamily,
389
+ // prevTest && !test: merge alternate
390
+ test: prev ? t.logicalExpression('&&', prev.test, negated) : negated,
391
+ consequent: prev
392
+ ? mergeProps(prev.alternate!, ternary.alternate)
393
+ : ternary.alternate,
394
+ remove,
395
+ alternate: null,
396
+ })
397
+
398
+ if (prev) {
399
+ expandedTernaries.push({
400
+ fontFamily,
401
+ test: t.logicalExpression(
402
+ '&&',
403
+ t.unaryExpression('!', prev.test),
404
+ ternary.test
405
+ ),
406
+ consequent: ternary.alternate,
407
+ remove,
408
+ alternate: null,
409
+ })
410
+ }
411
+ }
394
412
  }
395
413
 
396
- const comment = util.format(
397
- '/* %s:%s (%s) */',
398
- filePath,
399
- lineNumbers,
400
- originalNodeName
401
- )
414
+ let ternaryClassNameExpr: t.Expression | null = null
402
415
 
403
- for (const styleObject of finalStyles) {
404
- const identifier = styleObject[helpers.StyleObjectIdentifier]
405
- const rules = styleObject[helpers.StyleObjectRules]
406
- const className = `.${identifier}`
407
- if (cssMap.has(className)) {
408
- if (comment) {
409
- const val = cssMap.get(className)!
410
- val.commentTexts.push(comment)
411
- cssMap.set(className, val)
416
+ // next: create all CSS, build className strings and hoist, and create final node with props
417
+ if (hasTernaries) {
418
+ for (const ternary of expandedTernaries) {
419
+ if (!ternary.consequent) continue
420
+ const classNames = addStyles(ternary.consequent)
421
+ if (ternary.fontFamily) {
422
+ classNames.unshift(`font_${ternary.fontFamily}`)
423
+ }
424
+ const baseString = t.isStringLiteral(baseClassNameExpression)
425
+ ? baseClassNameExpression.value
426
+ : ''
427
+ const fullClassName =
428
+ (baseString ? `${baseString} ` : '') + classNames.join(' ')
429
+ const classNameLiteral = t.stringLiteral(fullClassName)
430
+
431
+ if (!ternaryClassNameExpr) {
432
+ ternaryClassNameExpr = classNameLiteral
433
+ } else {
434
+ ternaryClassNameExpr = t.conditionalExpression(
435
+ ternary.test,
436
+ classNameLiteral,
437
+ ternaryClassNameExpr
438
+ )
412
439
  }
413
- } else if (rules.length) {
414
- cssMap.set(className, {
415
- css: rules.join('\n'),
416
- commentTexts: [comment],
417
- })
418
440
  }
419
441
  }
442
+
443
+ let finalExpression: t.Expression | null =
444
+ ternaryClassNameExpr || baseClassNameExpression || null
445
+
446
+ // console.info('attrs', JSON.stringify(attrs, null, 2))
447
+ // console.info('expandedTernaries', JSON.stringify(expandedTernaries, null, 2))
448
+ // console.info('finalExpression', JSON.stringify(finalExpression, null, 2))
449
+ // console.info({ baseClassNameExpression })
450
+
451
+ if (finalExpression) {
452
+ // hoist to global variables
453
+ finalExpression = hoistClassNames(jsxPath, finalExpression)
454
+
455
+ const classNameProp = t.jsxAttribute(
456
+ t.jsxIdentifier('className'),
457
+ t.jsxExpressionContainer(finalExpression!)
458
+ )
459
+ finalAttrs.unshift(classNameProp)
460
+ }
461
+
462
+ node.attributes = finalAttrs
420
463
  },
421
464
  })
422
465
 
@@ -466,3 +509,56 @@ export async function extractToClassNames({
466
509
  map: result.map,
467
510
  }
468
511
  }
512
+
513
+ function hoistClassNames(path: NodePath<t.JSXElement>, expr: t.Expression) {
514
+ if (t.isStringLiteral(expr)) {
515
+ return hoistClassName(path, expr.value)
516
+ }
517
+
518
+ if (t.isBinaryExpression(expr)) {
519
+ const left = t.isStringLiteral(expr.left)
520
+ ? hoistClassName(path, expr.left.value)
521
+ : expr.left
522
+ const right = t.isStringLiteral(expr.right)
523
+ ? hoistClassName(path, expr.right.value)
524
+ : hoistClassNames(path, expr.right)
525
+ return t.binaryExpression(expr.operator, left, right)
526
+ }
527
+
528
+ if (t.isConditionalExpression(expr)) {
529
+ const cons = t.isStringLiteral(expr.consequent)
530
+ ? hoistClassName(path, expr.consequent.value)
531
+ : hoistClassNames(path, expr.consequent)
532
+
533
+ const alt = t.isStringLiteral(expr.alternate)
534
+ ? hoistClassName(path, expr.alternate.value)
535
+ : hoistClassNames(path, expr.alternate)
536
+
537
+ return t.conditionalExpression(expr.test, cons, alt)
538
+ }
539
+
540
+ return expr
541
+ }
542
+
543
+ function hoistClassName(path: NodePath<t.JSXElement>, str: string) {
544
+ const uid = path.scope.generateUidIdentifier('cn')
545
+ const parent = path.findParent((path) => path.isProgram())
546
+ if (!parent) throw new Error(`no program?`)
547
+ const variable = t.variableDeclaration('const', [
548
+ t.variableDeclarator(uid, t.stringLiteral(cleanupClassName(str))),
549
+ ])
550
+ // @ts-ignore
551
+ parent.unshiftContainer('body', variable)
552
+ return uid
553
+ }
554
+
555
+ function cleanupClassName(inStr: string) {
556
+ const out = new Set<string>()
557
+ for (const part of inStr.split(' ')) {
558
+ if (part === ' ') continue
559
+ if (part === 'font_') continue
560
+ out.add(part)
561
+ }
562
+ // always a space after for joining
563
+ return [...out].join(' ') + ' '
564
+ }