@tamagui/static 1.132.15 → 1.132.16-1754534218765

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