@tamagui/static 1.132.15 → 1.132.16-1754855349219

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