@tamagui/static 1.0.1-beta.59 → 1.0.1-beta.62

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 (48) hide show
  1. package/dist/cjs/extractor/createExtractor.js +49 -21
  2. package/dist/cjs/extractor/createExtractor.js.map +2 -2
  3. package/dist/cjs/extractor/extractMediaStyle.js +1 -1
  4. package/dist/cjs/extractor/extractMediaStyle.js.map +2 -2
  5. package/dist/cjs/extractor/extractToClassNames.js +1 -1
  6. package/dist/cjs/extractor/extractToClassNames.js.map +2 -2
  7. package/dist/esm/extractor/createExtractor.js +54 -24
  8. package/dist/esm/extractor/createExtractor.js.map +2 -2
  9. package/dist/esm/extractor/extractMediaStyle.js +1 -1
  10. package/dist/esm/extractor/extractMediaStyle.js.map +2 -2
  11. package/dist/esm/extractor/extractToClassNames.js +1 -1
  12. package/dist/esm/extractor/extractToClassNames.js.map +2 -2
  13. package/dist/jsx/extractor/createExtractor.js +54 -24
  14. package/dist/jsx/extractor/extractMediaStyle.js +1 -1
  15. package/dist/jsx/extractor/extractToClassNames.js +1 -1
  16. package/package.json +9 -7
  17. package/src/constants.ts +13 -0
  18. package/src/extractor/accessSafe.ts +18 -0
  19. package/src/extractor/babelParse.ts +27 -0
  20. package/src/extractor/buildClassName.ts +61 -0
  21. package/src/extractor/createEvaluator.ts +69 -0
  22. package/src/extractor/createExtractor.ts +1725 -0
  23. package/src/extractor/ensureImportingConcat.ts +39 -0
  24. package/src/extractor/evaluateAstNode.ts +121 -0
  25. package/src/extractor/extractHelpers.ts +119 -0
  26. package/src/extractor/extractMediaStyle.ts +192 -0
  27. package/src/extractor/extractToClassNames.ts +426 -0
  28. package/src/extractor/findTopmostFunction.ts +22 -0
  29. package/src/extractor/generatedUid.ts +43 -0
  30. package/src/extractor/getPrefixLogs.ts +6 -0
  31. package/src/extractor/getPropValueFromAttributes.ts +92 -0
  32. package/src/extractor/getSourceModule.ts +101 -0
  33. package/src/extractor/getStaticBindingsForScope.ts +183 -0
  34. package/src/extractor/hoistClassNames.ts +45 -0
  35. package/src/extractor/literalToAst.ts +84 -0
  36. package/src/extractor/loadTamagui.ts +139 -0
  37. package/src/extractor/logLines.ts +16 -0
  38. package/src/extractor/normalizeTernaries.ts +63 -0
  39. package/src/extractor/removeUnusedHooks.ts +76 -0
  40. package/src/extractor/timer.ts +18 -0
  41. package/src/extractor/validHTMLAttributes.ts +99 -0
  42. package/src/index.ts +9 -0
  43. package/src/patchReactNativeWeb.ts +165 -0
  44. package/src/types.ts +97 -0
  45. package/types/extractor/createExtractor.d.ts +14 -1
  46. package/types/extractor/createExtractor.d.ts.map +1 -1
  47. package/types/extractor/extractMediaStyle.d.ts.map +1 -1
  48. package/types/extractor/extractToClassNames.d.ts.map +1 -1
@@ -0,0 +1,1725 @@
1
+ import { relative } from 'path'
2
+
3
+ import traverse, { NodePath, Visitor } from '@babel/traverse'
4
+ import * as t from '@babel/types'
5
+ import {
6
+ PseudoStyles,
7
+ StaticConfigParsed,
8
+ TamaguiInternalConfig,
9
+ expandStyles,
10
+ getSplitStyles,
11
+ mediaQueryConfig,
12
+ normalizeStyleObject,
13
+ proxyThemeVariables,
14
+ pseudos,
15
+ rnw,
16
+ stylePropsTransform,
17
+ } from '@tamagui/core-node'
18
+ import { difference, pick } from 'lodash'
19
+ import type { ViewStyle } from 'react-native'
20
+
21
+ import { FAILED_EVAL } from '../constants'
22
+ import {
23
+ ExtractedAttr,
24
+ ExtractedAttrAttr,
25
+ ExtractedAttrStyle,
26
+ ExtractorParseProps,
27
+ Ternary,
28
+ } from '../types'
29
+ import { createEvaluator, createSafeEvaluator } from './createEvaluator'
30
+ import { evaluateAstNode } from './evaluateAstNode'
31
+ import { attrStr, findComponentName, isInsideTamagui, isPresent, objToStr } from './extractHelpers'
32
+ import { findTopmostFunction } from './findTopmostFunction'
33
+ import { getStaticBindingsForScope } from './getStaticBindingsForScope'
34
+ import { literalToAst } from './literalToAst'
35
+ import { loadTamagui } from './loadTamagui'
36
+ import { logLines } from './logLines'
37
+ import { normalizeTernaries } from './normalizeTernaries'
38
+ import { removeUnusedHooks } from './removeUnusedHooks'
39
+ import { timer } from './timer'
40
+ import { validHTMLAttributes } from './validHTMLAttributes'
41
+
42
+ const UNTOUCHED_PROPS = {
43
+ key: true,
44
+ style: true,
45
+ className: true,
46
+ }
47
+
48
+ const INLINE_EXTRACTABLE = {
49
+ ref: 'ref',
50
+ key: 'key',
51
+ onPress: 'onClick',
52
+ onHoverIn: 'onMouseEnter',
53
+ onHoverOut: 'onMouseLeave',
54
+ onPressIn: 'onMouseDown',
55
+ onPressOut: 'onMouseUp',
56
+ }
57
+
58
+ const isAttr = (x: ExtractedAttr): x is ExtractedAttrAttr => x.type === 'attr'
59
+
60
+ const validHooks = {
61
+ useMedia: true,
62
+ useTheme: true,
63
+ }
64
+
65
+ export type Extractor = ReturnType<typeof createExtractor>
66
+
67
+ const createTernary = (x: Ternary) => x
68
+
69
+ export function createExtractor() {
70
+ if (!process.env.TAMAGUI_TARGET) {
71
+ console.log('⚠️ Please set process.env.TAMAGUI_TARGET to either "web" or "native"')
72
+ process.exit(1)
73
+ }
74
+
75
+ const shouldAddDebugProp =
76
+ // really basic disable this for next.js because it messes with ssr
77
+ !process.env.npm_package_dependencies_next &&
78
+ process.env.TAMAGUI_TARGET !== 'native' &&
79
+ process.env.IDENTIFY_TAGS !== 'false' &&
80
+ (process.env.NODE_ENV === 'development' || process.env.DEBUG || process.env.IDENTIFY_TAGS)
81
+
82
+ let loadedTamaguiConfig: TamaguiInternalConfig
83
+ let hasLogged = false
84
+
85
+ return {
86
+ getTamagui() {
87
+ return loadedTamaguiConfig
88
+ },
89
+ parse: (
90
+ fileOrPath: NodePath<t.Program> | t.File,
91
+ {
92
+ config = 'tamagui.config.ts',
93
+ importsWhitelist = ['constants.js'],
94
+ evaluateVars = true,
95
+ shouldPrintDebug = false,
96
+ sourcePath = '',
97
+ onExtractTag,
98
+ getFlattenedNode,
99
+ disable,
100
+ disableExtraction,
101
+ disableExtractInlineMedia,
102
+ disableExtractVariables,
103
+ disableDebugAttr,
104
+ prefixLogs,
105
+ excludeProps,
106
+ target,
107
+ ...props
108
+ }: ExtractorParseProps
109
+ ) => {
110
+ if (disable) {
111
+ return null
112
+ }
113
+ if (sourcePath === '') {
114
+ throw new Error(`Must provide a source file name`)
115
+ }
116
+ if (!Array.isArray(props.components)) {
117
+ throw new Error(`Must provide components array with list of Tamagui component modules`)
118
+ }
119
+
120
+ const isTargetingHTML = target === 'html'
121
+ const ogDebug = shouldPrintDebug
122
+ const tm = timer()
123
+
124
+ // we require it after parse because we need to set some global/env stuff before importing
125
+ // otherwise we'd import `rnw` and cause it to evaluate react-native-web which causes errors
126
+ const { components, tamaguiConfig } = loadTamagui({
127
+ config,
128
+ components: props.components || ['tamagui'],
129
+ })
130
+
131
+ if (shouldPrintDebug === 'verbose') {
132
+ console.log('tamagui.config.ts:', { components, config })
133
+ }
134
+
135
+ tm.mark('load-tamagui', shouldPrintDebug === 'verbose')
136
+
137
+ loadedTamaguiConfig = tamaguiConfig as any
138
+
139
+ const proxiedTheme = proxyThemeVariables(
140
+ tamaguiConfig.themes[Object.keys(tamaguiConfig.themes)[0]]
141
+ )
142
+
143
+ type AccessListener = (key: string) => void
144
+ const themeAccessListeners = new Set<AccessListener>()
145
+ const defaultTheme = new Proxy(proxiedTheme, {
146
+ get(target, key) {
147
+ if (key[0] === '$') {
148
+ themeAccessListeners.forEach((cb) => cb(String(key)))
149
+ }
150
+ return Reflect.get(target, key)
151
+ },
152
+ })
153
+
154
+ const body = fileOrPath.type === 'Program' ? fileOrPath.get('body') : fileOrPath.program.body
155
+
156
+ /**
157
+ * Step 1: Determine if importing any statically extractable components
158
+ */
159
+ const isInternalImport = (importStr: string) => {
160
+ return isInsideTamagui(sourcePath) && importStr[0] === '.'
161
+ }
162
+
163
+ const validComponents: { [key: string]: any } = Object.keys(components)
164
+ // check if uppercase to avoid hitting media query proxy before init
165
+ .filter((key) => key[0].toUpperCase() === key[0] && !!components[key]?.staticConfig)
166
+ .reduce((obj, name) => {
167
+ obj[name] = components[name]
168
+ return obj
169
+ }, {})
170
+
171
+ if (shouldPrintDebug === 'verbose') {
172
+ console.log('validComponents', Object.keys(validComponents))
173
+ }
174
+
175
+ let doesUseValidImport = false
176
+ let hasImportedTheme = false
177
+
178
+ for (const bodyPath of body) {
179
+ if (bodyPath.type !== 'ImportDeclaration') continue
180
+ const node = ('node' in bodyPath ? bodyPath.node : bodyPath) as any
181
+ const from = node.source.value
182
+ const isValidImport = props.components.includes(from) || isInternalImport(from)
183
+ if (isValidImport) {
184
+ const isValidComponent = node.specifiers.some((specifier) => {
185
+ const name = specifier.local.name
186
+ return !!(validComponents[name] || validHooks[name])
187
+ })
188
+ if (shouldPrintDebug === 'verbose') {
189
+ console.log('import from', from, { isValidComponent })
190
+ }
191
+ if (isValidComponent) {
192
+ doesUseValidImport = true
193
+ break
194
+ }
195
+ }
196
+ }
197
+
198
+ if (shouldPrintDebug) {
199
+ console.log(sourcePath, { doesUseValidImport })
200
+ }
201
+
202
+ if (!doesUseValidImport) {
203
+ return null
204
+ }
205
+
206
+ tm.mark('import-check', shouldPrintDebug === 'verbose')
207
+
208
+ let couldntParse = false
209
+ const modifiedComponents = new Set<NodePath<any>>()
210
+
211
+ // only keeping a cache around per-file, reset it if it changes
212
+ const bindingCache: Record<string, string | null> = {}
213
+
214
+ const callTraverse = (a: Visitor<{}>) => {
215
+ return fileOrPath.type === 'File' ? traverse(fileOrPath, a) : fileOrPath.traverse(a)
216
+ }
217
+
218
+ /**
219
+ * Step 2: Statically extract from JSX < /> nodes
220
+ */
221
+ let programPath: NodePath<t.Program>
222
+
223
+ const res = {
224
+ flattened: 0,
225
+ optimized: 0,
226
+ modified: 0,
227
+ found: 0,
228
+ }
229
+
230
+ callTraverse({
231
+ Program: {
232
+ enter(path) {
233
+ programPath = path
234
+ },
235
+ },
236
+ JSXElement(traversePath) {
237
+ tm.mark('jsx-element', shouldPrintDebug === 'verbose')
238
+
239
+ const node = traversePath.node.openingElement
240
+ const ogAttributes = node.attributes
241
+ const componentName = findComponentName(traversePath.scope)
242
+ const closingElement = traversePath.node.closingElement
243
+
244
+ // skip non-identifier opening elements (member expressions, etc.)
245
+ if (t.isJSXMemberExpression(closingElement?.name) || !t.isJSXIdentifier(node.name)) {
246
+ return
247
+ }
248
+
249
+ // validate its a proper import from tamagui (or internally inside tamagui)
250
+ const binding = traversePath.scope.getBinding(node.name.name)
251
+
252
+ if (binding) {
253
+ if (!t.isImportDeclaration(binding.path.parent)) {
254
+ return
255
+ }
256
+ const source = binding.path.parent.source
257
+ if (!props.components.includes(source.value) && !isInternalImport(source.value)) {
258
+ return
259
+ }
260
+ if (!validComponents[binding.identifier.name]) {
261
+ return
262
+ }
263
+ }
264
+
265
+ const component = validComponents[node.name.name] as { staticConfig?: StaticConfigParsed }
266
+ if (!component || !component.staticConfig) {
267
+ return
268
+ }
269
+
270
+ const originalNodeName = node.name.name
271
+
272
+ // found a valid tag
273
+ res.found++
274
+
275
+ const filePath = `./${relative(process.cwd(), sourcePath)}`
276
+ const lineNumbers = node.loc
277
+ ? node.loc.start.line +
278
+ (node.loc.start.line !== node.loc.end.line ? `-${node.loc.end.line}` : '')
279
+ : ''
280
+ const tagId = [componentName, `${node.name.name}`, `${filePath}:${lineNumbers}`].filter(
281
+ Boolean
282
+ )
283
+
284
+ // debug just one
285
+ const debugPropValue = node.attributes
286
+ .filter<t.JSXAttribute>(
287
+ // @ts-ignore
288
+ (n) => t.isJSXAttribute(n) && t.isJSXIdentifier(n.name) && n.name.name === 'debug'
289
+ )
290
+ .map((n) => {
291
+ if (n.value === null) return true
292
+ if (t.isStringLiteral(n.value)) return n.value.value as 'verbose'
293
+ return false
294
+ })[0]
295
+
296
+ if (debugPropValue) {
297
+ shouldPrintDebug = debugPropValue
298
+ }
299
+
300
+ try {
301
+ node.attributes.find(
302
+ (n) =>
303
+ t.isJSXAttribute(n) &&
304
+ t.isJSXIdentifier(n.name) &&
305
+ n.name.name === 'debug' &&
306
+ n.value === null
307
+ )
308
+
309
+ if (shouldPrintDebug) {
310
+ console.log('\n')
311
+ console.log('\x1b[33m%s\x1b[0m', `${tagId[0]} | ${tagId[2]} -------------------`)
312
+ console.log('\x1b[1m', '\x1b[32m', `<${originalNodeName} />`)
313
+ }
314
+
315
+ // add data-is
316
+ if (shouldAddDebugProp && !disableDebugAttr) {
317
+ res.modified++
318
+ node.attributes.unshift(
319
+ t.jsxAttribute(t.jsxIdentifier('data-is'), t.stringLiteral(tagId.join(' ')))
320
+ )
321
+ }
322
+
323
+ const shouldLog = !hasLogged
324
+ if (shouldLog) {
325
+ const prefix = ' |'
326
+ console.log(
327
+ prefixLogs || prefix,
328
+ ' total · optimized · flattened '
329
+ )
330
+ hasLogged = true
331
+ }
332
+ if (disableExtraction) {
333
+ return
334
+ }
335
+
336
+ const { staticConfig } = component
337
+ const isTextView = staticConfig.isText || false
338
+ const validStyles = staticConfig?.validStyles ?? {}
339
+
340
+ function isValidStyleKey(name: string) {
341
+ return !!(
342
+ !!validStyles[name] ||
343
+ !!pseudos[name] ||
344
+ staticConfig.variants?.[name] ||
345
+ tamaguiConfig.shorthands[name] ||
346
+ (name[0] === '$' ? !!mediaQueryConfig[name.slice(1)] : false)
347
+ )
348
+ }
349
+
350
+ // find tag="a" tag="main" etc dom indicators
351
+ let tagName = staticConfig.defaultProps?.tag ?? (isTextView ? 'span' : 'div')
352
+ traversePath
353
+ .get('openingElement')
354
+ .get('attributes')
355
+ .forEach((path) => {
356
+ const attr = path.node
357
+ if (t.isJSXSpreadAttribute(attr)) return
358
+ if (attr.name.name !== 'tag') return
359
+ const val = attr.value
360
+ if (!t.isStringLiteral(val)) return
361
+ tagName = val.value
362
+ })
363
+
364
+ const flatNode = getFlattenedNode({ isTextView, tag: tagName })
365
+
366
+ const inlineProps = new Set([
367
+ ...(props.inlineProps || []),
368
+ ...(staticConfig.inlineProps || []),
369
+ ])
370
+
371
+ const deoptProps = new Set([
372
+ // always de-opt animation
373
+ 'animation',
374
+ ...(props.deoptProps || []),
375
+ ...(staticConfig.deoptProps || []),
376
+ ])
377
+
378
+ const inlineWhenUnflattened = new Set([...(staticConfig.inlineWhenUnflattened || [])])
379
+
380
+ // Generate scope object at this level
381
+ const staticNamespace = getStaticBindingsForScope(
382
+ traversePath.scope,
383
+ importsWhitelist,
384
+ sourcePath,
385
+ bindingCache,
386
+ shouldPrintDebug
387
+ )
388
+
389
+ const attemptEval = !evaluateVars
390
+ ? evaluateAstNode
391
+ : createEvaluator({
392
+ // @ts-ignore
393
+ tamaguiConfig,
394
+ staticNamespace,
395
+ sourcePath,
396
+ traversePath,
397
+ shouldPrintDebug,
398
+ })
399
+ const attemptEvalSafe = createSafeEvaluator(attemptEval)
400
+
401
+ if (shouldPrintDebug) {
402
+ console.log(' staticNamespace', Object.keys(staticNamespace).join(', '))
403
+ }
404
+
405
+ //
406
+ // SPREADS SETUP
407
+ //
408
+
409
+ // TODO restore
410
+ // const hasDeopt = (obj: Object) => {
411
+ // return Object.keys(obj).some(isDeoptedProp)
412
+ // }
413
+
414
+ // flatten any easily evaluatable spreads
415
+ const flattenedAttrs: (t.JSXAttribute | t.JSXSpreadAttribute)[] = []
416
+ traversePath
417
+ .get('openingElement')
418
+ .get('attributes')
419
+ .forEach((path) => {
420
+ const attr = path.node
421
+ if (!t.isJSXSpreadAttribute(attr)) {
422
+ flattenedAttrs.push(attr)
423
+ return
424
+ }
425
+ let arg: any
426
+ try {
427
+ arg = attemptEval(attr.argument)
428
+ } catch (e: any) {
429
+ if (shouldPrintDebug) {
430
+ console.log(' couldnt parse spread', e.message)
431
+ }
432
+ flattenedAttrs.push(attr)
433
+ return
434
+ }
435
+ if (arg !== undefined) {
436
+ try {
437
+ if (typeof arg !== 'object' || arg == null) {
438
+ if (shouldPrintDebug) {
439
+ console.log(' non object or null arg', arg)
440
+ }
441
+ flattenedAttrs.push(attr)
442
+ } else {
443
+ for (const k in arg) {
444
+ const value = arg[k]
445
+ // this is a null prop:
446
+ if (!value && typeof value === 'object') {
447
+ console.log('shouldnt we handle this?', k, value, arg)
448
+ continue
449
+ }
450
+ flattenedAttrs.push(
451
+ t.jsxAttribute(
452
+ t.jsxIdentifier(k),
453
+ t.jsxExpressionContainer(literalToAst(value))
454
+ )
455
+ )
456
+ }
457
+ }
458
+ } catch (err) {
459
+ console.warn('cant parse spread, caught err', err)
460
+ couldntParse = true
461
+ }
462
+ }
463
+ })
464
+
465
+ if (couldntParse) {
466
+ return
467
+ }
468
+
469
+ tm.mark('jsx-element-flattened', shouldPrintDebug === 'verbose')
470
+
471
+ // set flattened
472
+ node.attributes = flattenedAttrs
473
+
474
+ // add in NON-STYLE default props
475
+ if (staticConfig.defaultProps) {
476
+ for (const key in staticConfig.defaultProps) {
477
+ if (isValidStyleKey(key)) {
478
+ continue
479
+ }
480
+ const serialize = require('babel-literal-to-ast')
481
+ const val = staticConfig.defaultProps[key]
482
+ try {
483
+ const serializedDefaultProp = serialize(val)
484
+ node.attributes.unshift(
485
+ t.jsxAttribute(
486
+ t.jsxIdentifier(key),
487
+ typeof val === 'string'
488
+ ? t.stringLiteral(val)
489
+ : t.jsxExpressionContainer(serializedDefaultProp)
490
+ )
491
+ )
492
+ } catch (err) {
493
+ console.warn(
494
+ `⚠️ Error evaluating default prop for component ${node.name.name}, prop ${key}\n error: ${err}\n value:`,
495
+ val,
496
+ '\n defaultProps:',
497
+ staticConfig.defaultProps
498
+ )
499
+ }
500
+ }
501
+ }
502
+
503
+ let attrs: ExtractedAttr[] = []
504
+ let shouldDeopt = false
505
+ const inlined = new Map<string, any>()
506
+ let hasSetOptimized = false
507
+ const inlineWhenUnflattenedOGVals = {}
508
+
509
+ // RUN first pass
510
+
511
+ // normalize all conditionals so we can evaluate away easier later
512
+ // at the same time lets normalize shorthand media queries into spreads:
513
+ // that way we can parse them with the same logic later on
514
+ //
515
+ // {...media.sm && { color: x ? 'red' : 'blue' }}
516
+ // => {...media.sm && x && { color: 'red' }}
517
+ // => {...media.sm && !x && { color: 'blue' }}
518
+ //
519
+ // $sm={{ color: 'red' }}
520
+ // => {...media.sm && { color: 'red' }}
521
+ //
522
+ // $sm={{ color: x ? 'red' : 'blue' }}
523
+ // => {...media.sm && x && { color: 'red' }}
524
+ // => {...media.sm && !x && { color: 'blue' }}
525
+
526
+ attrs = traversePath
527
+ .get('openingElement')
528
+ .get('attributes')
529
+ .flatMap((path) => {
530
+ try {
531
+ const res = evaluateAttribute(path)
532
+ tm.mark('jsx-element-evaluate-attr', shouldPrintDebug === 'verbose')
533
+ if (!res) {
534
+ path.remove()
535
+ }
536
+ return res
537
+ } catch (err: any) {
538
+ if (shouldPrintDebug) {
539
+ console.log('Error extracting attribute', err.message, err.stack)
540
+ console.log('node', path.node)
541
+ }
542
+ return {
543
+ type: 'attr',
544
+ value: path.node,
545
+ } as const
546
+ }
547
+ })
548
+ .flat(4)
549
+ .filter(isPresent)
550
+
551
+ if (shouldPrintDebug) {
552
+ console.log(' - attrs (before):\n', logLines(attrs.map(attrStr).join(', ')))
553
+ }
554
+
555
+ // START function evaluateAttribute
556
+ function evaluateAttribute(
557
+ path: NodePath<t.JSXAttribute | t.JSXSpreadAttribute>
558
+ ): ExtractedAttr | ExtractedAttr[] | null {
559
+ const attribute = path.node
560
+ const attr: ExtractedAttr = { type: 'attr', value: attribute }
561
+ // ...spreads
562
+ if (t.isJSXSpreadAttribute(attribute)) {
563
+ const arg = attribute.argument
564
+ const conditional = t.isConditionalExpression(arg)
565
+ ? // <YStack {...isSmall ? { color: 'red } : { color: 'blue }}
566
+ ([arg.test, arg.consequent, arg.alternate] as const)
567
+ : t.isLogicalExpression(arg) && arg.operator === '&&'
568
+ ? // <YStack {...isSmall && { color: 'red }}
569
+ ([arg.left, arg.right, null] as const)
570
+ : null
571
+
572
+ if (conditional) {
573
+ const [test, alt, cons] = conditional
574
+ if (!test) throw new Error(`no test`)
575
+ if ([alt, cons].some((side) => side && !isExtractable(side))) {
576
+ if (shouldPrintDebug) {
577
+ console.log('not extractable', alt, cons)
578
+ }
579
+ return attr
580
+ }
581
+ // split into individual ternaries per object property
582
+ return [
583
+ ...(createTernariesFromObjectProperties(test, alt) || []),
584
+ ...((cons &&
585
+ createTernariesFromObjectProperties(t.unaryExpression('!', test), cons)) ||
586
+ []),
587
+ ].map((ternary) => ({
588
+ type: 'ternary',
589
+ value: ternary,
590
+ }))
591
+ }
592
+ }
593
+ // END ...spreads
594
+
595
+ // directly keep these
596
+ // couldn't evaluate spread, undefined name, or name is not string
597
+ if (
598
+ t.isJSXSpreadAttribute(attribute) ||
599
+ !attribute.name ||
600
+ typeof attribute.name.name !== 'string'
601
+ ) {
602
+ if (shouldPrintDebug) {
603
+ console.log(' ! inlining, spread attr')
604
+ }
605
+ inlined.set(`${Math.random()}`, 'spread')
606
+ return attr
607
+ }
608
+
609
+ const name = attribute.name.name
610
+
611
+ if (excludeProps?.has(name)) {
612
+ if (shouldPrintDebug) {
613
+ console.log(' excluding prop', name)
614
+ }
615
+ return null
616
+ }
617
+
618
+ if (inlineProps.has(name)) {
619
+ inlined.set(name, name)
620
+ if (shouldPrintDebug) {
621
+ console.log(' ! inlining, inline prop', name)
622
+ }
623
+ return attr
624
+ }
625
+
626
+ // can still optimize the object... see hoverStyle on native
627
+ if (deoptProps.has(name)) {
628
+ shouldDeopt = true
629
+ inlined.set(name, name)
630
+ if (shouldPrintDebug) {
631
+ console.log(' ! inlining, deopted prop', name)
632
+ }
633
+ return attr
634
+ }
635
+
636
+ // pass className, key, and style props through untouched
637
+ if (UNTOUCHED_PROPS[name]) {
638
+ return attr
639
+ }
640
+
641
+ if (INLINE_EXTRACTABLE[name]) {
642
+ inlined.set(name, INLINE_EXTRACTABLE[name])
643
+ return attr
644
+ }
645
+
646
+ if (name.startsWith('data-')) {
647
+ return attr
648
+ }
649
+
650
+ // shorthand media queries
651
+ if (name[0] === '$' && t.isJSXExpressionContainer(attribute?.value)) {
652
+ // allow disabling this extraction
653
+ if (disableExtractInlineMedia) {
654
+ return attr
655
+ }
656
+
657
+ const shortname = name.slice(1)
658
+ if (mediaQueryConfig[shortname]) {
659
+ const expression = attribute.value.expression
660
+ if (!t.isJSXEmptyExpression(expression)) {
661
+ const ternaries = createTernariesFromObjectProperties(
662
+ t.stringLiteral(shortname),
663
+ expression,
664
+ {
665
+ inlineMediaQuery: shortname,
666
+ }
667
+ )
668
+ if (ternaries) {
669
+ return ternaries.map((value) => ({
670
+ type: 'ternary',
671
+ value,
672
+ }))
673
+ }
674
+ }
675
+ }
676
+ }
677
+
678
+ const [value, valuePath] = (() => {
679
+ if (t.isJSXExpressionContainer(attribute?.value)) {
680
+ return [attribute.value.expression!, path.get('value')!] as const
681
+ } else {
682
+ return [attribute.value!, path.get('value')!] as const
683
+ }
684
+ })()
685
+
686
+ const remove = () => {
687
+ Array.isArray(valuePath) ? valuePath.map((p) => p.remove()) : valuePath.remove()
688
+ }
689
+
690
+ if (name === 'ref') {
691
+ if (shouldPrintDebug) {
692
+ console.log(' ! inlining, ref', name)
693
+ }
694
+ inlined.set('ref', 'ref')
695
+ return attr
696
+ }
697
+
698
+ if (name === 'tag') {
699
+ return {
700
+ type: 'attr',
701
+ value: path.node,
702
+ }
703
+ }
704
+
705
+ // native shouldn't extract variables
706
+ if (disableExtractVariables) {
707
+ if (value) {
708
+ if (value.type === 'StringLiteral' && value.value[0] === '$') {
709
+ if (shouldPrintDebug) {
710
+ console.log(` ! inlining, native disable extract: ${name} =`, value.value)
711
+ }
712
+ inlined.set(name, true)
713
+ return attr
714
+ }
715
+ }
716
+ }
717
+
718
+ if (name === 'theme') {
719
+ inlined.set('theme', attr.value)
720
+ return attr
721
+ }
722
+
723
+ // if value can be evaluated, extract it and filter it out
724
+ const styleValue = attemptEvalSafe(value)
725
+
726
+ // never flatten if a prop isn't a valid static attribute
727
+ // only post prop-mapping
728
+ if (!isValidStyleKey(name)) {
729
+ let keys = [name]
730
+ let out: any = null
731
+ if (staticConfig.propMapper) {
732
+ // for now passing empty props {}, a bit odd, need to at least document
733
+ // for now we don't expose custom components so just noting behavior
734
+ out = staticConfig.propMapper(
735
+ name,
736
+ styleValue,
737
+ defaultTheme,
738
+ staticConfig.defaultProps,
739
+ { resolveVariablesAs: 'auto' },
740
+ undefined,
741
+ shouldPrintDebug
742
+ )
743
+ if (out) {
744
+ if (!Array.isArray(out)) {
745
+ console.warn(`Error expected array but got`, out)
746
+ couldntParse = true
747
+ shouldDeopt = true
748
+ } else {
749
+ out = Object.fromEntries(out)
750
+ }
751
+ }
752
+ if (out) {
753
+ if (isTargetingHTML) {
754
+ // translate to DOM-compat
755
+ out = rnw.createDOMProps(isTextView ? 'span' : 'div', out)
756
+ // remove className - we dont use rnw styling
757
+ delete out.className
758
+ }
759
+
760
+ keys = Object.keys(out)
761
+ }
762
+ }
763
+
764
+ let didInline = false
765
+ const attributes = keys.map((key) => {
766
+ const val = out[key]
767
+ if (isValidStyleKey(key)) {
768
+ return {
769
+ type: 'style',
770
+ value: { [name]: styleValue },
771
+ name,
772
+ attr: path.node,
773
+ } as const
774
+ }
775
+ if (
776
+ validHTMLAttributes[key] ||
777
+ key.startsWith('aria-') ||
778
+ key.startsWith('data-')
779
+ ) {
780
+ return attr
781
+ }
782
+ if (shouldPrintDebug) {
783
+ console.log(' ! inlining, non-static', key)
784
+ }
785
+ didInline = true
786
+ inlined.set(key, val)
787
+ return val
788
+ })
789
+
790
+ // weird logic whats going on here
791
+ if (didInline) {
792
+ if (shouldPrintDebug) {
793
+ console.log(' bailing flattening due to attributes', attributes)
794
+ }
795
+ // bail
796
+ return attr
797
+ }
798
+
799
+ // return evaluated attributes
800
+ return attributes
801
+ }
802
+
803
+ // FAILED = dynamic or ternary, keep going
804
+ if (styleValue !== FAILED_EVAL) {
805
+ if (inlineWhenUnflattened.has(name)) {
806
+ // preserve original value for restoration
807
+ inlineWhenUnflattenedOGVals[name] = { styleValue, attr }
808
+ }
809
+
810
+ if (isValidStyleKey(name)) {
811
+ if (shouldPrintDebug) {
812
+ console.log(` style: ${name} =`, styleValue)
813
+ }
814
+ if (!(name in staticConfig.defaultProps)) {
815
+ if (!hasSetOptimized) {
816
+ res.optimized++
817
+ hasSetOptimized = true
818
+ }
819
+ }
820
+ return {
821
+ type: 'style',
822
+ value: { [name]: styleValue },
823
+ name,
824
+ attr: path.node,
825
+ }
826
+ } else {
827
+ inlined.set(name, true)
828
+ return attr
829
+ }
830
+ }
831
+
832
+ // ternaries!
833
+
834
+ // binary ternary, we can eventually make this smarter but step 1
835
+ // basically for the common use case of:
836
+ // opacity={(conditional ? 0 : 1) * scale}
837
+ if (t.isBinaryExpression(value)) {
838
+ if (shouldPrintDebug) {
839
+ console.log(` binary expression ${name} = `, value)
840
+ }
841
+ const { operator, left, right } = value
842
+ // if one side is a ternary, and the other side is evaluatable, we can maybe extract
843
+ const lVal = attemptEvalSafe(left)
844
+ const rVal = attemptEvalSafe(right)
845
+ if (shouldPrintDebug) {
846
+ console.log(` evalBinaryExpression lVal ${String(lVal)}, rVal ${String(rVal)}`)
847
+ }
848
+ if (lVal !== FAILED_EVAL && t.isConditionalExpression(right)) {
849
+ const ternary = addBinaryConditional(operator, left, right)
850
+ if (ternary) return ternary
851
+ }
852
+ if (rVal !== FAILED_EVAL && t.isConditionalExpression(left)) {
853
+ const ternary = addBinaryConditional(operator, right, left)
854
+ if (ternary) return ternary
855
+ }
856
+ if (shouldPrintDebug) {
857
+ console.log(` evalBinaryExpression cant extract`)
858
+ }
859
+ inlined.set(name, true)
860
+ return attr
861
+ }
862
+
863
+ const staticConditional = getStaticConditional(value)
864
+ if (staticConditional) {
865
+ if (shouldPrintDebug === 'verbose') {
866
+ console.log(` static conditional ${name}`, value)
867
+ }
868
+ return { type: 'ternary', value: staticConditional }
869
+ }
870
+
871
+ const staticLogical = getStaticLogical(value)
872
+ if (staticLogical) {
873
+ if (shouldPrintDebug === 'verbose') {
874
+ console.log(` static ternary ${name} = `, value)
875
+ }
876
+ return { type: 'ternary', value: staticLogical }
877
+ }
878
+
879
+ // if we've made it this far, the prop stays inline
880
+ inlined.set(name, true)
881
+ if (shouldPrintDebug) {
882
+ console.log(` ! inline no match ${name}`, value)
883
+ }
884
+
885
+ //
886
+ // RETURN ATTR
887
+ //
888
+ return attr
889
+
890
+ // attr helpers:
891
+ function addBinaryConditional(
892
+ operator: any,
893
+ staticExpr: any,
894
+ cond: t.ConditionalExpression
895
+ ): ExtractedAttr | null {
896
+ if (getStaticConditional(cond)) {
897
+ const alt = attemptEval(t.binaryExpression(operator, staticExpr, cond.alternate))
898
+ const cons = attemptEval(
899
+ t.binaryExpression(operator, staticExpr, cond.consequent)
900
+ )
901
+ if (shouldPrintDebug) {
902
+ console.log(' binaryConditional', cond.test, cons, alt)
903
+ }
904
+ return {
905
+ type: 'ternary',
906
+ value: {
907
+ test: cond.test,
908
+ remove,
909
+ alternate: { [name]: alt },
910
+ consequent: { [name]: cons },
911
+ },
912
+ }
913
+ }
914
+ return null
915
+ }
916
+
917
+ function getStaticConditional(value: t.Node): Ternary | null {
918
+ if (t.isConditionalExpression(value)) {
919
+ try {
920
+ const aVal = attemptEval(value.alternate)
921
+ const cVal = attemptEval(value.consequent)
922
+ if (shouldPrintDebug) {
923
+ const type = value.test.type
924
+ console.log(' static ternary', type, cVal, aVal)
925
+ }
926
+ return {
927
+ test: value.test,
928
+ remove,
929
+ consequent: { [name]: cVal },
930
+ alternate: { [name]: aVal },
931
+ }
932
+ } catch (err: any) {
933
+ if (shouldPrintDebug) {
934
+ console.log(' cant eval ternary', err.message)
935
+ }
936
+ }
937
+ }
938
+ return null
939
+ }
940
+
941
+ function getStaticLogical(value: t.Node): Ternary | null {
942
+ if (t.isLogicalExpression(value)) {
943
+ if (value.operator === '&&') {
944
+ try {
945
+ const val = attemptEval(value.right)
946
+ if (shouldPrintDebug) {
947
+ console.log(' staticLogical', value.left, name, val)
948
+ }
949
+ return {
950
+ test: value.left,
951
+ remove,
952
+ consequent: { [name]: val },
953
+ alternate: null,
954
+ }
955
+ } catch (err) {
956
+ if (shouldPrintDebug) {
957
+ console.log(' cant static eval logical', err)
958
+ }
959
+ }
960
+ }
961
+ }
962
+ return null
963
+ }
964
+ } // END function evaluateAttribute
965
+
966
+ function isExtractable(obj: t.Node): obj is t.ObjectExpression {
967
+ return (
968
+ t.isObjectExpression(obj) &&
969
+ obj.properties.every((prop) => {
970
+ if (!t.isObjectProperty(prop)) {
971
+ console.log('not object prop', prop)
972
+ return false
973
+ }
974
+ const propName = prop.key['name']
975
+ if (!isValidStyleKey(propName) && propName !== 'tag') {
976
+ if (shouldPrintDebug) {
977
+ console.log(' not a valid style prop!', propName)
978
+ }
979
+ return false
980
+ }
981
+ return true
982
+ })
983
+ )
984
+ }
985
+
986
+ // side = {
987
+ // color: 'red',
988
+ // background: x ? 'red' : 'green',
989
+ // $gtSm: { color: 'green' }
990
+ // }
991
+ // => Ternary<test, { color: 'red' }, null>
992
+ // => Ternary<test && x, { background: 'red' }, null>
993
+ // => Ternary<test && !x, { background: 'green' }, null>
994
+ // => Ternary<test && '$gtSm', { color: 'green' }, null>
995
+ function createTernariesFromObjectProperties(
996
+ test: t.Expression,
997
+ side: t.Expression | null,
998
+ ternaryPartial: Partial<Ternary> = {}
999
+ ): null | Ternary[] {
1000
+ if (!side) {
1001
+ return null
1002
+ }
1003
+ if (!isExtractable(side)) {
1004
+ throw new Error('not extractable')
1005
+ }
1006
+ return side.properties.flatMap((property) => {
1007
+ if (!t.isObjectProperty(property)) {
1008
+ throw new Error('expected object property')
1009
+ }
1010
+ // handle media queries inside spread/conditional objects
1011
+ if (t.isIdentifier(property.key)) {
1012
+ const key = property.key.name
1013
+ const mediaQueryKey = key.slice(1)
1014
+ const isMediaQuery = key[0] === '$' && mediaQueryConfig[mediaQueryKey]
1015
+ if (isMediaQuery) {
1016
+ if (t.isExpression(property.value)) {
1017
+ const ternaries = createTernariesFromObjectProperties(
1018
+ t.stringLiteral(mediaQueryKey),
1019
+ property.value,
1020
+ {
1021
+ inlineMediaQuery: mediaQueryKey,
1022
+ }
1023
+ )
1024
+ if (ternaries) {
1025
+ return ternaries.map((value) => ({
1026
+ ...ternaryPartial,
1027
+ ...value,
1028
+ // ensure media query test stays on left side (see getMediaQueryTernary)
1029
+ test: t.logicalExpression('&&', value.test, test),
1030
+ }))
1031
+ } else {
1032
+ console.log('⚠️ no ternaries?', property)
1033
+ }
1034
+ } else {
1035
+ console.log('⚠️ not expression', property)
1036
+ }
1037
+ }
1038
+ }
1039
+ // this could be a recurse here if we want to get fancy
1040
+ if (t.isConditionalExpression(property.value)) {
1041
+ // merge up into the parent conditional, split into two
1042
+ const [truthy, falsy] = [
1043
+ t.objectExpression([t.objectProperty(property.key, property.value.consequent)]),
1044
+ t.objectExpression([t.objectProperty(property.key, property.value.alternate)]),
1045
+ ].map((x) => attemptEval(x))
1046
+ return [
1047
+ createTernary({
1048
+ remove() {},
1049
+ ...ternaryPartial,
1050
+ test: t.logicalExpression('&&', test, property.value.test),
1051
+ consequent: truthy,
1052
+ alternate: null,
1053
+ }),
1054
+ createTernary({
1055
+ ...ternaryPartial,
1056
+ test: t.logicalExpression(
1057
+ '&&',
1058
+ test,
1059
+ t.unaryExpression('!', property.value.test)
1060
+ ),
1061
+ consequent: falsy,
1062
+ alternate: null,
1063
+ remove() {},
1064
+ }),
1065
+ ]
1066
+ }
1067
+ const obj = t.objectExpression([t.objectProperty(property.key, property.value)])
1068
+ const consequent = attemptEval(obj)
1069
+ return createTernary({
1070
+ remove() {},
1071
+ ...ternaryPartial,
1072
+ test,
1073
+ consequent,
1074
+ alternate: null,
1075
+ })
1076
+ })
1077
+ }
1078
+
1079
+ // now update to new values
1080
+ node.attributes = attrs.filter(isAttr).map((x) => x.value)
1081
+
1082
+ if (couldntParse || shouldDeopt) {
1083
+ if (shouldPrintDebug) {
1084
+ console.log(` avoid optimizing:`, { couldntParse, shouldDeopt })
1085
+ }
1086
+ node.attributes = ogAttributes
1087
+ return
1088
+ }
1089
+
1090
+ // before deopt, can still optimize
1091
+ const parentFn = findTopmostFunction(traversePath)
1092
+ if (parentFn) {
1093
+ modifiedComponents.add(parentFn)
1094
+ }
1095
+
1096
+ // combine ternaries
1097
+ let ternaries: Ternary[] = []
1098
+ attrs = attrs
1099
+ .reduce<(ExtractedAttr | ExtractedAttr[])[]>((out, cur) => {
1100
+ const next = attrs[attrs.indexOf(cur) + 1]
1101
+ if (cur.type === 'ternary') {
1102
+ ternaries.push(cur.value)
1103
+ }
1104
+ if ((!next || next.type !== 'ternary') && ternaries.length) {
1105
+ // finish, process
1106
+ const normalized = normalizeTernaries(ternaries).map(
1107
+ ({ alternate, consequent, ...rest }) => {
1108
+ return {
1109
+ type: 'ternary' as const,
1110
+ value: {
1111
+ ...rest,
1112
+ alternate: alternate || null,
1113
+ consequent: consequent || null,
1114
+ },
1115
+ }
1116
+ }
1117
+ )
1118
+ try {
1119
+ return [...out, ...normalized]
1120
+ } finally {
1121
+ if (shouldPrintDebug) {
1122
+ console.log(
1123
+ ` normalizeTernaries (${ternaries.length} => ${normalized.length})`
1124
+ )
1125
+ }
1126
+ ternaries = []
1127
+ }
1128
+ }
1129
+ if (cur.type === 'ternary') {
1130
+ return out
1131
+ }
1132
+ out.push(cur)
1133
+ return out
1134
+ }, [])
1135
+ .flat()
1136
+
1137
+ // flatten logic!
1138
+ // fairly simple check to see if all children are text
1139
+ const hasSpread = node.attributes.some((x) => t.isJSXSpreadAttribute(x))
1140
+
1141
+ const hasOnlyStringChildren =
1142
+ !hasSpread &&
1143
+ (node.selfClosing ||
1144
+ (traversePath.node.children &&
1145
+ traversePath.node.children.every((x) => x.type === 'JSXText')))
1146
+
1147
+ let themeVal = inlined.get('theme')
1148
+ inlined.delete('theme')
1149
+ const allOtherPropsExtractable = [...inlined].every(([k, v]) => INLINE_EXTRACTABLE[k])
1150
+ const shouldWrapThme = allOtherPropsExtractable && !!themeVal
1151
+ const canFlattenProps = inlined.size === 0 || shouldWrapThme || allOtherPropsExtractable
1152
+
1153
+ let shouldFlatten =
1154
+ !shouldDeopt &&
1155
+ canFlattenProps &&
1156
+ !hasSpread &&
1157
+ staticConfig.neverFlatten !== true &&
1158
+ (staticConfig.neverFlatten === 'jsx' ? hasOnlyStringChildren : true)
1159
+
1160
+ if (disableExtractVariables) {
1161
+ themeAccessListeners.add((key) => {
1162
+ shouldFlatten = false
1163
+ if (shouldPrintDebug) {
1164
+ console.log(' ! accessing theme key, avoid flatten', key)
1165
+ }
1166
+ })
1167
+ }
1168
+
1169
+ if (shouldPrintDebug) {
1170
+ // prettier-ignore
1171
+ console.log(' - flatten?', objToStr({ hasSpread, shouldDeopt, shouldFlatten, canFlattenProps, shouldWrapThme, allOtherPropsExtractable, hasOnlyStringChildren }))
1172
+ }
1173
+
1174
+ // wrap theme around children on flatten
1175
+ if (shouldFlatten && shouldWrapThme) {
1176
+ if (shouldPrintDebug) {
1177
+ console.log(' - wrapping theme', allOtherPropsExtractable, themeVal)
1178
+ }
1179
+
1180
+ // remove theme attribute from flattened node
1181
+ attrs = attrs.filter((x) =>
1182
+ x.type === 'attr' && t.isJSXAttribute(x.value) && x.value.name.name === 'theme'
1183
+ ? false
1184
+ : true
1185
+ )
1186
+
1187
+ // add import
1188
+ if (!hasImportedTheme) {
1189
+ hasImportedTheme = true
1190
+ programPath.node.body.push(
1191
+ t.importDeclaration(
1192
+ [t.importSpecifier(t.identifier('_TamaguiTheme'), t.identifier('Theme'))],
1193
+ t.stringLiteral('@tamagui/core')
1194
+ )
1195
+ )
1196
+ }
1197
+
1198
+ traversePath.replaceWith(
1199
+ t.jsxElement(
1200
+ t.jsxOpeningElement(t.jsxIdentifier('_TamaguiTheme'), [
1201
+ t.jsxAttribute(t.jsxIdentifier('name'), themeVal.value),
1202
+ ]),
1203
+ t.jsxClosingElement(t.jsxIdentifier('_TamaguiTheme')),
1204
+ [traversePath.node]
1205
+ )
1206
+ )
1207
+ }
1208
+
1209
+ // only if we flatten, ensure the default styles are there
1210
+ if (shouldFlatten && staticConfig.defaultProps) {
1211
+ const defaultStyleAttrs = Object.keys(staticConfig.defaultProps).flatMap((key) => {
1212
+ if (!isValidStyleKey(key)) {
1213
+ return []
1214
+ }
1215
+ const value = staticConfig.defaultProps[key]
1216
+ const name = tamaguiConfig.shorthands[key] || key
1217
+ if (value === undefined) {
1218
+ console.warn(
1219
+ `⚠️ Error evaluating default style for component, prop ${key} ${value}`
1220
+ )
1221
+ shouldDeopt = true
1222
+ return
1223
+ }
1224
+ const attr: ExtractedAttrStyle = {
1225
+ type: 'style',
1226
+ name,
1227
+ value: { [name]: value },
1228
+ }
1229
+ return attr
1230
+ }) as ExtractedAttr[]
1231
+
1232
+ if (defaultStyleAttrs.length) {
1233
+ attrs = [...defaultStyleAttrs, ...attrs]
1234
+ }
1235
+ }
1236
+
1237
+ if (shouldDeopt) {
1238
+ node.attributes = ogAttributes
1239
+ return
1240
+ }
1241
+
1242
+ // insert overrides - this inserts null props for things that are set in classNames
1243
+ // only when not flattening, so the downstream component can skip applying those styles
1244
+ const ensureOverridden = {}
1245
+ if (!shouldFlatten) {
1246
+ for (const cur of attrs) {
1247
+ if (cur.type === 'style') {
1248
+ // TODO need to loop over initial props not just style props
1249
+ for (const key in cur.value) {
1250
+ const shouldEnsureOverridden = !!staticConfig.ensureOverriddenProp?.[key]
1251
+ const isSetInAttrsAlready = attrs.some(
1252
+ (x) =>
1253
+ x.type === 'attr' &&
1254
+ x.value.type === 'JSXAttribute' &&
1255
+ x.value.name.name === key
1256
+ )
1257
+
1258
+ if (!isSetInAttrsAlready) {
1259
+ const isVariant = !!staticConfig.variants?.[cur.name || '']
1260
+ if (isVariant || shouldEnsureOverridden) {
1261
+ ensureOverridden[key] = true
1262
+ }
1263
+ }
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+
1269
+ if (shouldPrintDebug) {
1270
+ console.log(' - attrs (flattened): \n', logLines(attrs.map(attrStr).join(', ')))
1271
+ console.log(' - ensureOverriden:', Object.keys(ensureOverridden).join(', '))
1272
+ }
1273
+
1274
+ // expand shorthands, de-opt variables
1275
+ attrs = attrs.reduce<ExtractedAttr[]>((acc, cur) => {
1276
+ if (!cur) return acc
1277
+ if (cur.type === 'attr' && !t.isJSXSpreadAttribute(cur.value)) {
1278
+ if (shouldFlatten) {
1279
+ if (cur.value.name.name === 'tag') {
1280
+ // remove tag=""
1281
+ return acc
1282
+ }
1283
+ }
1284
+ }
1285
+ if (cur.type !== 'style') {
1286
+ acc.push(cur)
1287
+ return acc
1288
+ }
1289
+
1290
+ let key = Object.keys(cur.value)[0]
1291
+ const value = cur.value[key]
1292
+ const fullKey = tamaguiConfig.shorthands[key]
1293
+ // expand shorthands
1294
+ if (fullKey) {
1295
+ cur.value = { [fullKey]: value }
1296
+ key = fullKey
1297
+ }
1298
+
1299
+ // finally we have all styles + expansions, lets see if we need to skip
1300
+ // any and keep them as attrs
1301
+ if (disableExtractVariables) {
1302
+ if (value[0] === '$') {
1303
+ if (shouldPrintDebug) {
1304
+ console.log(` keeping variable inline: ${key} =`, value)
1305
+ }
1306
+ acc.push({
1307
+ type: 'attr',
1308
+ value: t.jsxAttribute(
1309
+ t.jsxIdentifier(key),
1310
+ t.jsxExpressionContainer(t.stringLiteral(value))
1311
+ ),
1312
+ })
1313
+ return acc
1314
+ }
1315
+ }
1316
+
1317
+ acc.push(cur)
1318
+ return acc
1319
+ }, [])
1320
+
1321
+ tm.mark('jsx-element-expanded', shouldPrintDebug === 'verbose')
1322
+ if (shouldPrintDebug) {
1323
+ console.log(' - attrs (expanded): \n', logLines(attrs.map(attrStr).join(', ')))
1324
+ }
1325
+
1326
+ // merge styles, leave undefined values
1327
+ let prev: ExtractedAttr | null = null
1328
+
1329
+ function mergeStyles(prev: ViewStyle & PseudoStyles, next: ViewStyle & PseudoStyles) {
1330
+ normalizeStyleObject(next)
1331
+ for (const key in next) {
1332
+ // merge pseudos
1333
+ if (pseudos[key]) {
1334
+ prev[key] = prev[key] || {}
1335
+ if (shouldPrintDebug) {
1336
+ if (!next[key] || !prev[key]) {
1337
+ console.log('warn: missing', key, prev, next)
1338
+ }
1339
+ }
1340
+ Object.assign(prev[key], next[key])
1341
+ } else {
1342
+ prev[key] = next[key]
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ attrs = attrs.reduce<ExtractedAttr[]>((acc, cur) => {
1348
+ if (cur.type === 'style') {
1349
+ const key = Object.keys(cur.value)[0]
1350
+ const value = cur.value[key]
1351
+
1352
+ const shouldKeepOriginalAttr =
1353
+ // !isStyleAndAttr[key] &&
1354
+ !shouldFlatten &&
1355
+ // de-opt transform styles so it merges properly if not flattened
1356
+ // we handle this later on
1357
+ // (stylePropsTransform[key] ||
1358
+ // de-opt if non-style
1359
+ !validStyles[key] &&
1360
+ !pseudos[key] &&
1361
+ !key.startsWith('data-')
1362
+
1363
+ if (shouldKeepOriginalAttr) {
1364
+ if (shouldPrintDebug) {
1365
+ console.log(' - keeping as non-style', key)
1366
+ }
1367
+ prev = cur
1368
+ acc.push({
1369
+ type: 'attr',
1370
+ value: t.jsxAttribute(
1371
+ t.jsxIdentifier(key),
1372
+ t.jsxExpressionContainer(
1373
+ typeof value === 'string' ? t.stringLiteral(value) : literalToAst(value)
1374
+ )
1375
+ ),
1376
+ })
1377
+ acc.push(cur)
1378
+ return acc
1379
+ }
1380
+
1381
+ if (ensureOverridden[key]) {
1382
+ acc.push({
1383
+ type: 'attr',
1384
+ value:
1385
+ cur.attr ||
1386
+ t.jsxAttribute(
1387
+ t.jsxIdentifier(key),
1388
+ t.jsxExpressionContainer(t.nullLiteral())
1389
+ ),
1390
+ })
1391
+ }
1392
+
1393
+ if (prev?.type === 'style') {
1394
+ mergeStyles(prev.value, cur.value)
1395
+ return acc
1396
+ }
1397
+ }
1398
+
1399
+ prev = cur
1400
+ acc.push(cur)
1401
+ return acc
1402
+ }, [])
1403
+
1404
+ const state = {
1405
+ noClassNames: true,
1406
+ focus: false,
1407
+ hover: false,
1408
+ mounted: true, // TODO match logic in createComponent
1409
+ press: false,
1410
+ pressIn: false,
1411
+ }
1412
+
1413
+ // evaluates all static attributes into a simple object
1414
+ let foundStaticProps = {}
1415
+ for (const key in attrs) {
1416
+ const cur = attrs[key]
1417
+ if (cur.type === 'style') {
1418
+ normalizeStyleObject(cur.value)
1419
+ foundStaticProps = {
1420
+ ...foundStaticProps,
1421
+ ...expandStyles(cur.value),
1422
+ }
1423
+ continue
1424
+ }
1425
+ if (cur.type === 'attr') {
1426
+ if (t.isJSXSpreadAttribute(cur.value)) {
1427
+ continue
1428
+ }
1429
+ if (!t.isJSXIdentifier(cur.value.name)) {
1430
+ continue
1431
+ }
1432
+ const key = cur.value.name.name
1433
+ // undefined = boolean true
1434
+ const value = attemptEvalSafe(cur.value.value || t.booleanLiteral(true))
1435
+ if (value !== FAILED_EVAL) {
1436
+ foundStaticProps = {
1437
+ ...foundStaticProps,
1438
+ [key]: value,
1439
+ }
1440
+ }
1441
+ }
1442
+ }
1443
+
1444
+ // must preserve exact order
1445
+ const completeProps = {}
1446
+ for (const key in staticConfig.defaultProps) {
1447
+ if (!(key in foundStaticProps)) {
1448
+ completeProps[key] = staticConfig.defaultProps[key]
1449
+ }
1450
+ }
1451
+ for (const key in foundStaticProps) {
1452
+ completeProps[key] = foundStaticProps[key]
1453
+ }
1454
+
1455
+ if (shouldPrintDebug) {
1456
+ console.log(' - attrs (combined 🔀): \n', logLines(attrs.map(attrStr).join(', ')))
1457
+ console.log(' - defaultProps: \n', logLines(objToStr(staticConfig.defaultProps)))
1458
+ // prettier-ignore
1459
+ console.log(' - foundStaticProps: \n', logLines(objToStr(foundStaticProps)))
1460
+ console.log(' - completeProps: \n', logLines(objToStr(completeProps)))
1461
+ }
1462
+
1463
+ // post process
1464
+ const getStyles = (props: Object | null, debugName = '') => {
1465
+ if (!props || !Object.keys(props).length) {
1466
+ if (shouldPrintDebug) console.log(' getStyles() no props')
1467
+ return {}
1468
+ }
1469
+ if (excludeProps && !!excludeProps.size) {
1470
+ for (const key in props) {
1471
+ if (excludeProps.has(key)) {
1472
+ if (shouldPrintDebug) console.log(' delete excluded', key)
1473
+ delete props[key]
1474
+ }
1475
+ }
1476
+ }
1477
+ try {
1478
+ const out = getSplitStyles(
1479
+ props,
1480
+ staticConfig,
1481
+ defaultTheme,
1482
+ {
1483
+ ...state,
1484
+ fallbackProps: completeProps,
1485
+ },
1486
+ undefined,
1487
+ props['debug']
1488
+ )
1489
+ const outStyle = {
1490
+ ...out.style,
1491
+ ...out.pseudos,
1492
+ }
1493
+ omitInvalidStyles(outStyle)
1494
+ if (shouldPrintDebug) {
1495
+ // prettier-ignore
1496
+ console.log(` getStyles ${debugName} (props):\n`, logLines(objToStr(props)))
1497
+ // prettier-ignore
1498
+ console.log(` getStyles ${debugName} (out.viewProps):\n`, logLines(objToStr(out.viewProps)))
1499
+ // prettier-ignore
1500
+ console.log(` getStyles ${debugName} (out.style):\n`, logLines(objToStr(outStyle || {}), true))
1501
+ }
1502
+ return outStyle
1503
+ } catch (err: any) {
1504
+ console.log('error', err.message, err.stack)
1505
+ return {}
1506
+ }
1507
+ }
1508
+
1509
+ function omitInvalidStyles(style: any) {
1510
+ if (staticConfig.validStyles) {
1511
+ for (const key in style) {
1512
+ if (
1513
+ stylePropsTransform[key] ||
1514
+ (!staticConfig.validStyles[key] &&
1515
+ !pseudos[key] &&
1516
+ !/(hoverStyle|focusStyle|pressStyle)$/.test(key))
1517
+ ) {
1518
+ if (shouldPrintDebug) console.log(' delete invalid style', key)
1519
+ delete style[key]
1520
+ }
1521
+ }
1522
+ }
1523
+ }
1524
+
1525
+ // used to ensure we pass the entire prop bundle to getStyles
1526
+ const completeStyles = getStyles(completeProps, 'completeStyles')
1527
+
1528
+ if (!completeStyles) {
1529
+ throw new Error(`Impossible, no styles`)
1530
+ }
1531
+
1532
+ // any extra styles added in postprocess should be added to first group as they wont be overriden
1533
+ const addInitialStyleKeys = shouldFlatten
1534
+ ? difference(Object.keys(completeStyles), Object.keys(foundStaticProps))
1535
+ : []
1536
+
1537
+ if (addInitialStyleKeys.length) {
1538
+ const toAdd = pick(completeStyles, ...addInitialStyleKeys)
1539
+ const firstGroup = attrs.find((x) => x.type === 'style')
1540
+ if (shouldPrintDebug) {
1541
+ console.log(' toAdd', objToStr(toAdd))
1542
+ }
1543
+ if (!firstGroup) {
1544
+ attrs.unshift({ type: 'style', value: toAdd })
1545
+ } else {
1546
+ // because were adding fully processed, remove any unprocessed from first group
1547
+ omitInvalidStyles(firstGroup.value)
1548
+ Object.assign(firstGroup.value, toAdd)
1549
+ }
1550
+ }
1551
+
1552
+ if (shouldPrintDebug) {
1553
+ // prettier-ignore
1554
+ console.log(' -- addInitialStyleKeys', addInitialStyleKeys.join(', '), { shouldFlatten })
1555
+ // prettier-ignore
1556
+ console.log(' -- completeStyles:\n', logLines(objToStr(completeStyles)))
1557
+ }
1558
+
1559
+ let getStyleError: any = null
1560
+
1561
+ // fix up ternaries, combine final style values
1562
+ for (const attr of attrs) {
1563
+ try {
1564
+ switch (attr.type) {
1565
+ case 'ternary':
1566
+ const a = getStyles(attr.value.alternate, 'ternary.alternate')
1567
+ const c = getStyles(attr.value.consequent, 'ternary.consequent')
1568
+ if (a) attr.value.alternate = a
1569
+ if (c) attr.value.consequent = c
1570
+ if (shouldPrintDebug) console.log(' => tern ', attrStr(attr))
1571
+ continue
1572
+ case 'style':
1573
+ if (shouldPrintDebug) console.log(' * styles in', attr.value)
1574
+ // expand variants and such
1575
+ // get the keys we need
1576
+ const styles = getStyles(attr.value, 'style')
1577
+ // prettier-ignore
1578
+ if (shouldPrintDebug) console.log(' * styles out', logLines(objToStr(styles)))
1579
+ if (styles) {
1580
+ // but actually resolve them to the full object
1581
+ // TODO media/psuedo merging
1582
+ attr.value = Object.fromEntries(
1583
+ Object.keys(styles).map((k) => [k, completeStyles[k]])
1584
+ )
1585
+ }
1586
+ continue
1587
+ }
1588
+ } catch (err) {
1589
+ // any error de-opt
1590
+ getStyleError = err
1591
+ }
1592
+ }
1593
+
1594
+ if (shouldPrintDebug) {
1595
+ // prettier-ignore
1596
+ console.log(' - attrs (ternaries/combined):\n', logLines(attrs.map(attrStr).join(', ')))
1597
+ }
1598
+
1599
+ tm.mark('jsx-element-styles', shouldPrintDebug === 'verbose')
1600
+
1601
+ if (getStyleError) {
1602
+ console.log(' ⚠️ postprocessing error, deopt', getStyleError)
1603
+ node.attributes = ogAttributes
1604
+ return node
1605
+ }
1606
+
1607
+ // final lazy extra loop:
1608
+ const existingStyleKeys = new Set()
1609
+ for (let i = attrs.length - 1; i >= 0; i--) {
1610
+ const attr = attrs[i]
1611
+
1612
+ // if flattening map inline props to proper flattened names
1613
+ if (shouldFlatten && canFlattenProps) {
1614
+ if (attr.type === 'attr') {
1615
+ if (t.isJSXAttribute(attr.value)) {
1616
+ if (t.isJSXIdentifier(attr.value.name)) {
1617
+ const name = attr.value.name.name
1618
+ if (INLINE_EXTRACTABLE[name]) {
1619
+ // map to HTML only name
1620
+ attr.value.name.name = INLINE_EXTRACTABLE[name]
1621
+ }
1622
+ }
1623
+ }
1624
+ }
1625
+ }
1626
+
1627
+ // remove duplicate styles
1628
+ // so if you have:
1629
+ // style({ color: 'red' }), ...someProps, style({ color: 'green' })
1630
+ // this will mutate:
1631
+ // style({}), ...someProps, style({ color: 'green' })
1632
+ if (attr.type === 'style') {
1633
+ for (const key in attr.value) {
1634
+ if (existingStyleKeys.has(key)) {
1635
+ if (shouldPrintDebug) {
1636
+ console.log(' >> delete existing', key)
1637
+ }
1638
+ delete attr.value[key]
1639
+ } else {
1640
+ existingStyleKeys.add(key)
1641
+ }
1642
+ }
1643
+ }
1644
+ }
1645
+
1646
+ // inlineWhenUnflattened
1647
+ if (!shouldFlatten) {
1648
+ if (Object.keys(inlineWhenUnflattenedOGVals).length) {
1649
+ for (const [index, attr] of attrs.entries()) {
1650
+ if (attr.type === 'style') {
1651
+ for (const key in attr.value) {
1652
+ const val = inlineWhenUnflattenedOGVals[key]
1653
+ if (val) {
1654
+ // delete the style
1655
+ delete attr.value[key]
1656
+
1657
+ // and insert it before
1658
+ attrs.splice(index - 1, 0, val.attr)
1659
+ }
1660
+ }
1661
+ }
1662
+ }
1663
+ }
1664
+ }
1665
+
1666
+ if (shouldFlatten) {
1667
+ // DO FLATTEN
1668
+ if (shouldPrintDebug) {
1669
+ console.log(' [✅] flattening', originalNodeName, flatNode)
1670
+ }
1671
+ node.name.name = flatNode
1672
+ res.flattened++
1673
+ if (closingElement) {
1674
+ closingElement.name.name = flatNode
1675
+ }
1676
+ }
1677
+
1678
+ if (shouldPrintDebug) {
1679
+ // prettier-ignore
1680
+ console.log(` ❊❊ inline props (${inlined.size}):`, shouldDeopt ? ' deopted' : '', hasSpread ? ' has spread' : '', staticConfig.neverFlatten ? 'neverFlatten' : '')
1681
+ console.log(' - attrs (end):\n', logLines(attrs.map(attrStr).join(', ')))
1682
+ }
1683
+
1684
+ onExtractTag({
1685
+ attrs,
1686
+ node,
1687
+ lineNumbers,
1688
+ filePath,
1689
+ attemptEval,
1690
+ jsxPath: traversePath,
1691
+ originalNodeName,
1692
+ isFlattened: shouldFlatten,
1693
+ programPath,
1694
+ })
1695
+ } catch (err) {
1696
+ throw err
1697
+ } finally {
1698
+ if (debugPropValue) {
1699
+ shouldPrintDebug = ogDebug
1700
+ }
1701
+ }
1702
+ },
1703
+ })
1704
+
1705
+ tm.mark('jsx-done', shouldPrintDebug === 'verbose')
1706
+
1707
+ /**
1708
+ * Step 3: Remove dead code from removed media query / theme hooks
1709
+ */
1710
+ if (modifiedComponents.size) {
1711
+ const all = Array.from(modifiedComponents)
1712
+ if (shouldPrintDebug) {
1713
+ console.log(' [🪝] hook check', all.length)
1714
+ }
1715
+ for (const comp of all) {
1716
+ removeUnusedHooks(comp, shouldPrintDebug)
1717
+ }
1718
+ }
1719
+
1720
+ tm.done(shouldPrintDebug === 'verbose')
1721
+
1722
+ return res
1723
+ },
1724
+ }
1725
+ }