@tamagui/static 1.0.1-beta.57 → 1.0.1-beta.61

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