@tamagui/static 1.0.0-alpha.0

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 (76) hide show
  1. package/README.md +21 -0
  2. package/dist/constants.js +10 -0
  3. package/dist/constants.js.map +7 -0
  4. package/dist/extractor/accessSafe.js +11 -0
  5. package/dist/extractor/accessSafe.js.map +7 -0
  6. package/dist/extractor/babelParse.js +30 -0
  7. package/dist/extractor/babelParse.js.map +7 -0
  8. package/dist/extractor/buildClassName.js +40 -0
  9. package/dist/extractor/buildClassName.js.map +7 -0
  10. package/dist/extractor/createEvaluator.js +51 -0
  11. package/dist/extractor/createEvaluator.js.map +7 -0
  12. package/dist/extractor/createExtractor.js +801 -0
  13. package/dist/extractor/createExtractor.js.map +7 -0
  14. package/dist/extractor/ensureImportingConcat.js +24 -0
  15. package/dist/extractor/ensureImportingConcat.js.map +7 -0
  16. package/dist/extractor/evaluateAstNode.js +94 -0
  17. package/dist/extractor/evaluateAstNode.js.map +7 -0
  18. package/dist/extractor/extractHelpers.js +92 -0
  19. package/dist/extractor/extractHelpers.js.map +7 -0
  20. package/dist/extractor/extractMediaStyle.js +151 -0
  21. package/dist/extractor/extractMediaStyle.js.map +7 -0
  22. package/dist/extractor/extractToClassNames.js +243 -0
  23. package/dist/extractor/extractToClassNames.js.map +7 -0
  24. package/dist/extractor/findTopmostFunction.js +23 -0
  25. package/dist/extractor/findTopmostFunction.js.map +7 -0
  26. package/dist/extractor/generatedUid.js +28 -0
  27. package/dist/extractor/generatedUid.js.map +7 -0
  28. package/dist/extractor/getPropValueFromAttributes.js +49 -0
  29. package/dist/extractor/getPropValueFromAttributes.js.map +7 -0
  30. package/dist/extractor/getSourceModule.js +69 -0
  31. package/dist/extractor/getSourceModule.js.map +7 -0
  32. package/dist/extractor/getStaticBindingsForScope.js +128 -0
  33. package/dist/extractor/getStaticBindingsForScope.js.map +7 -0
  34. package/dist/extractor/hoistClassNames.js +44 -0
  35. package/dist/extractor/hoistClassNames.js.map +7 -0
  36. package/dist/extractor/literalToAst.js +34 -0
  37. package/dist/extractor/literalToAst.js.map +7 -0
  38. package/dist/extractor/loadTamagui.js +41 -0
  39. package/dist/extractor/loadTamagui.js.map +7 -0
  40. package/dist/extractor/normalizeTernaries.js +52 -0
  41. package/dist/extractor/normalizeTernaries.js.map +7 -0
  42. package/dist/extractor/removeUnusedHooks.js +78 -0
  43. package/dist/extractor/removeUnusedHooks.js.map +7 -0
  44. package/dist/index.cjs +2079 -0
  45. package/dist/index.cjs.map +7 -0
  46. package/dist/index.js +14 -0
  47. package/dist/index.js.map +7 -0
  48. package/dist/patchReactNativeWeb.js +92 -0
  49. package/dist/patchReactNativeWeb.js.map +7 -0
  50. package/dist/types.js +1 -0
  51. package/dist/types.js.map +7 -0
  52. package/package.json +72 -0
  53. package/src/constants.ts +10 -0
  54. package/src/extractor/accessSafe.ts +18 -0
  55. package/src/extractor/babelParse.ts +27 -0
  56. package/src/extractor/buildClassName.ts +61 -0
  57. package/src/extractor/createEvaluator.ts +68 -0
  58. package/src/extractor/createExtractor.ts +1113 -0
  59. package/src/extractor/ensureImportingConcat.ts +33 -0
  60. package/src/extractor/evaluateAstNode.ts +121 -0
  61. package/src/extractor/extractHelpers.ts +93 -0
  62. package/src/extractor/extractMediaStyle.ts +191 -0
  63. package/src/extractor/extractToClassNames.ts +337 -0
  64. package/src/extractor/findTopmostFunction.ts +22 -0
  65. package/src/extractor/generatedUid.ts +43 -0
  66. package/src/extractor/getPropValueFromAttributes.ts +92 -0
  67. package/src/extractor/getSourceModule.ts +101 -0
  68. package/src/extractor/getStaticBindingsForScope.ts +173 -0
  69. package/src/extractor/hoistClassNames.ts +45 -0
  70. package/src/extractor/literalToAst.ts +32 -0
  71. package/src/extractor/loadTamagui.ts +61 -0
  72. package/src/extractor/normalizeTernaries.ts +60 -0
  73. package/src/extractor/removeUnusedHooks.ts +76 -0
  74. package/src/index.ts +9 -0
  75. package/src/patchReactNativeWeb.ts +107 -0
  76. package/src/types.ts +80 -0
@@ -0,0 +1,1113 @@
1
+ import traverse, { NodePath, Visitor } from '@babel/traverse'
2
+ import * as t from '@babel/types'
3
+ import type { StaticConfigParsed, TamaguiInternalConfig } from '@tamagui/core'
4
+ import * as CoreNode from '@tamagui/core-node'
5
+ import { stylePropsTransform } from '@tamagui/helpers'
6
+ import { difference, pick } from 'lodash'
7
+
8
+ import { ExtractedAttr, ExtractedAttrAttr, ExtractorParseProps, Ternary } from '../types'
9
+ import { createEvaluator, createSafeEvaluator } from './createEvaluator'
10
+ import { evaluateAstNode } from './evaluateAstNode'
11
+ import { attrStr, findComponentName, isInsideTamagui, isPresent, objToStr } from './extractHelpers'
12
+ import { findTopmostFunction } from './findTopmostFunction'
13
+ import { getStaticBindingsForScope } from './getStaticBindingsForScope'
14
+ import { literalToAst } from './literalToAst'
15
+ import { loadTamagui } from './loadTamagui'
16
+ import { normalizeTernaries } from './normalizeTernaries'
17
+ import { removeUnusedHooks } from './removeUnusedHooks'
18
+
19
+ const { mediaQueryConfig, postProcessStyles, pseudos } = CoreNode
20
+
21
+ export const FAILED_EVAL = Symbol('failed_style_eval')
22
+ const UNTOUCHED_PROPS = {
23
+ key: true,
24
+ style: true,
25
+ className: true,
26
+ }
27
+
28
+ const isAttr = (x: ExtractedAttr): x is ExtractedAttrAttr => x.type === 'attr'
29
+
30
+ const validHooks = {
31
+ useMedia: true,
32
+ useTheme: true,
33
+ }
34
+
35
+ export type Extractor = ReturnType<typeof createExtractor>
36
+
37
+ const createTernary = (x: Ternary) => x
38
+
39
+ export function createExtractor() {
40
+ const shouldAddDebugProp =
41
+ // really basic disable this for next.js because it messes with ssr
42
+ !process.env.npm_package_dependencies_next &&
43
+ process.env.TAMAGUI_TARGET !== 'native' &&
44
+ process.env.IDENTIFY_TAGS !== 'false' &&
45
+ (process.env.NODE_ENV === 'development' || process.env.DEBUG || process.env.IDENTIFY_TAGS)
46
+
47
+ // ts imports
48
+ require('esbuild-register/dist/node').register({
49
+ target: 'es2019',
50
+ format: 'cjs',
51
+ })
52
+
53
+ let loadedTamaguiConfig: TamaguiInternalConfig
54
+
55
+ return {
56
+ getTamaguiConfig() {
57
+ return loadedTamaguiConfig
58
+ },
59
+ parse: (
60
+ fileOrPath: NodePath<t.Program> | t.File,
61
+ {
62
+ config = 'tamagui.config.ts',
63
+ importsWhitelist = ['constants.js'],
64
+ evaluateVars = true,
65
+ shouldPrintDebug = false,
66
+ sourcePath = '',
67
+ onExtractTag,
68
+ getFlattenedNode,
69
+ onDidFlatten,
70
+ ...props
71
+ }: ExtractorParseProps
72
+ ) => {
73
+ if (sourcePath === '') {
74
+ throw new Error(`Must provide a source file name`)
75
+ }
76
+ if (!Array.isArray(props.components)) {
77
+ throw new Error(`Must provide components array with list of Tamagui component modules`)
78
+ }
79
+
80
+ // we require it after parse because we need to set some global/env stuff before importing
81
+ // otherwise we'd import `rnw` and cause it to evaluate react-native-web which causes errors
82
+ const { components, tamaguiConfig } = loadTamagui({
83
+ config,
84
+ components: props.components || ['tamagui'],
85
+ })
86
+
87
+ loadedTamaguiConfig = tamaguiConfig
88
+
89
+ const defaultTheme = tamaguiConfig.themes[Object.keys(tamaguiConfig.themes)[0]]
90
+
91
+ // TODO this can be passed in / set based on if source changed
92
+ // const shouldReCheckTheme = false //Date.now() - hasParsedFileLast > 600
93
+ // hasParsedFileLast = Date.now()
94
+
95
+ let doesUseValidImport = false
96
+ const body = fileOrPath.type === 'Program' ? fileOrPath.get('body') : fileOrPath.program.body
97
+
98
+ /**
99
+ * Step 1: Determine if importing any statically extractable components
100
+ */
101
+ const isInternalImport = (importStr: string) =>
102
+ isInsideTamagui(sourcePath) && importStr[0] === '.'
103
+ const validComponents: { [key: string]: any } = Object.keys(components)
104
+ .filter((key) => !!components[key]?.staticConfig)
105
+ .reduce((obj, name) => {
106
+ obj[name] = components[name]
107
+ return obj
108
+ }, {})
109
+
110
+ for (const bodyPath of body) {
111
+ if (bodyPath.type !== 'ImportDeclaration') continue
112
+ const node = ('node' in bodyPath ? bodyPath.node : bodyPath) as any
113
+ const from = node.source.value
114
+ if (props.components.includes(from) || isInternalImport(from)) {
115
+ if (
116
+ node.specifiers.some((specifier) => {
117
+ const name = specifier.local.name
118
+ return validComponents[name] || validHooks[name]
119
+ })
120
+ ) {
121
+ doesUseValidImport = true
122
+ break
123
+ }
124
+ }
125
+ }
126
+
127
+ if (shouldPrintDebug) {
128
+ console.log(sourcePath, { doesUseValidImport })
129
+ }
130
+
131
+ if (!doesUseValidImport) {
132
+ return null
133
+ }
134
+
135
+ let couldntParse = false
136
+ const modifiedComponents = new Set<NodePath<any>>()
137
+
138
+ // only keeping a cache around per-file, reset it if it changes
139
+ const bindingCache: Record<string, string | null> = {}
140
+
141
+ const callTraverse = (a: Visitor<{}>) => {
142
+ return fileOrPath.type === 'File' ? traverse(fileOrPath, a) : fileOrPath.traverse(a)
143
+ }
144
+
145
+ /**
146
+ * Step 2: Statically extract from JSX < /> nodes
147
+ */
148
+ let programPath: NodePath<t.Program>
149
+
150
+ callTraverse({
151
+ Program: {
152
+ enter(path) {
153
+ programPath = path
154
+ },
155
+ },
156
+ JSXElement(traversePath) {
157
+ const node = traversePath.node.openingElement
158
+ const ogAttributes = node.attributes
159
+ const componentName = findComponentName(traversePath.scope)
160
+ const closingElement = traversePath.node.closingElement
161
+
162
+ // skip non-identifier opening elements (member expressions, etc.)
163
+ if (t.isJSXMemberExpression(closingElement?.name) || !t.isJSXIdentifier(node.name)) {
164
+ return
165
+ }
166
+
167
+ // validate its a proper import from tamagui (or internally inside tamagui)
168
+ const binding = traversePath.scope.getBinding(node.name.name)
169
+
170
+ if (binding) {
171
+ if (!t.isImportDeclaration(binding.path.parent)) {
172
+ return
173
+ }
174
+ const source = binding.path.parent.source
175
+ if (!props.components.includes(source.value) && !isInternalImport(source.value)) {
176
+ return
177
+ }
178
+ if (!validComponents[binding.identifier.name]) {
179
+ return
180
+ }
181
+ }
182
+
183
+ const component = validComponents[node.name.name] as { staticConfig?: StaticConfigParsed }
184
+ if (!component || !component.staticConfig) {
185
+ return
186
+ }
187
+
188
+ const { staticConfig } = component
189
+ const originalNodeName = node.name.name
190
+ const isTextView = staticConfig.isText || false
191
+ const validStyles = staticConfig?.validStyles ?? {}
192
+
193
+ // find tag="a" tag="main" etc dom indicators
194
+ let tagName = staticConfig.defaultProps?.tag ?? (isTextView ? 'span' : 'div')
195
+ traversePath
196
+ .get('openingElement')
197
+ .get('attributes')
198
+ .forEach((path) => {
199
+ const attr = path.node
200
+ if (t.isJSXSpreadAttribute(attr)) return
201
+ if (attr.name.name !== 'tag') return
202
+ const val = attr.value
203
+ if (!t.isStringLiteral(val)) return
204
+ tagName = val.value
205
+ })
206
+
207
+ const flatNode = getFlattenedNode({ isTextView, tag: tagName })
208
+
209
+ const deoptProps = new Set([
210
+ ...(props.deoptProps ?? []),
211
+ ...(staticConfig.deoptProps ?? []),
212
+ ])
213
+ const excludeProps = new Set(props.excludeProps ?? [])
214
+ const isExcludedProp = (name: string) => {
215
+ const res = excludeProps.has(name)
216
+ if (res && shouldPrintDebug) console.log(` excluding ${name}`)
217
+ return res
218
+ }
219
+
220
+ const isDeoptedProp = (name: string) => {
221
+ const res = deoptProps.has(name)
222
+ if (res && shouldPrintDebug) console.log(` deopting ${name}`)
223
+ return res
224
+ }
225
+
226
+ if (shouldPrintDebug) {
227
+ console.log(`\n<${originalNodeName} />`)
228
+ }
229
+
230
+ // Generate scope object at this level
231
+ const staticNamespace = getStaticBindingsForScope(
232
+ traversePath.scope,
233
+ importsWhitelist,
234
+ sourcePath,
235
+ bindingCache,
236
+ shouldPrintDebug
237
+ )
238
+
239
+ const attemptEval = !evaluateVars
240
+ ? evaluateAstNode
241
+ : createEvaluator({
242
+ tamaguiConfig,
243
+ staticNamespace,
244
+ sourcePath,
245
+ traversePath,
246
+ shouldPrintDebug,
247
+ })
248
+ const attemptEvalSafe = createSafeEvaluator(attemptEval)
249
+
250
+ if (shouldPrintDebug) {
251
+ console.log(' staticNamespace', Object.keys(staticNamespace).join(', '))
252
+ }
253
+
254
+ //
255
+ // SPREADS SETUP
256
+ //
257
+
258
+ // TODO restore
259
+ const hasDeopt = (obj: Object) => {
260
+ return Object.keys(obj).some(isDeoptedProp)
261
+ }
262
+
263
+ // flatten any easily evaluatable spreads
264
+ const flattenedAttrs: (t.JSXAttribute | t.JSXSpreadAttribute)[] = []
265
+ traversePath
266
+ .get('openingElement')
267
+ .get('attributes')
268
+ .forEach((path) => {
269
+ const attr = path.node
270
+ if (!t.isJSXSpreadAttribute(attr)) {
271
+ flattenedAttrs.push(attr)
272
+ return
273
+ }
274
+ let arg: any
275
+ try {
276
+ arg = attemptEval(attr.argument)
277
+ } catch (e: any) {
278
+ if (shouldPrintDebug) {
279
+ console.log(' couldnt parse spread', e.message)
280
+ }
281
+ flattenedAttrs.push(attr)
282
+ return
283
+ }
284
+ if (typeof arg !== 'undefined') {
285
+ try {
286
+ if (typeof arg !== 'object' || arg == null) {
287
+ if (shouldPrintDebug) {
288
+ console.log(' non object or null arg', arg)
289
+ }
290
+ flattenedAttrs.push(attr)
291
+ } else {
292
+ for (const k in arg) {
293
+ const value = arg[k]
294
+ // this is a null prop:
295
+ if (!value && typeof value === 'object') {
296
+ console.log('shouldnt we handle this?', k, value, arg)
297
+ continue
298
+ }
299
+ flattenedAttrs.push(
300
+ t.jsxAttribute(
301
+ t.jsxIdentifier(k),
302
+ t.jsxExpressionContainer(literalToAst(value))
303
+ )
304
+ )
305
+ }
306
+ }
307
+ } catch (err) {
308
+ console.warn('cant parse spread, caught err', err)
309
+ couldntParse = true
310
+ }
311
+ }
312
+ })
313
+
314
+ if (couldntParse) {
315
+ return
316
+ }
317
+
318
+ // set flattened
319
+ node.attributes = flattenedAttrs
320
+
321
+ // add in default props
322
+ if (staticConfig.defaultProps) {
323
+ for (const key in staticConfig.defaultProps) {
324
+ const serialize = require('babel-literal-to-ast')
325
+ const val = staticConfig.defaultProps[key]
326
+ node.attributes.unshift(
327
+ t.jsxAttribute(
328
+ t.jsxIdentifier(key),
329
+ typeof val === 'string'
330
+ ? t.stringLiteral(val)
331
+ : t.jsxExpressionContainer(serialize(val))
332
+ )
333
+ )
334
+ }
335
+ }
336
+
337
+ let attrs: ExtractedAttr[] = []
338
+ let shouldDeopt = false
339
+ let inlinePropCount = 0
340
+ let isFlattened = false
341
+
342
+ // RUN first pass
343
+
344
+ // normalize all conditionals so we can evaluate away easier later
345
+ // at the same time lets normalize shorthand media queries into spreads:
346
+ // that way we can parse them with the same logic later on
347
+ //
348
+ // {...media.sm && { color: x ? 'red' : 'blue' }}
349
+ // => {...media.sm && x && { color: 'red' }}
350
+ // => {...media.sm && !x && { color: 'blue' }}
351
+ //
352
+ // $sm={{ color: 'red' }}
353
+ // => {...media.sm && { color: 'red' }}
354
+ //
355
+ // $sm={{ color: x ? 'red' : 'blue' }}
356
+ // => {...media.sm && x && { color: 'red' }}
357
+ // => {...media.sm && !x && { color: 'blue' }}
358
+
359
+ attrs = traversePath
360
+ .get('openingElement')
361
+ .get('attributes')
362
+ .flatMap((path) => {
363
+ try {
364
+ const res = evaluateAttribute(path)
365
+ if (!res) {
366
+ path.remove()
367
+ }
368
+ return res
369
+ } catch (err: any) {
370
+ console.log('Error extracting attribute', err.message, err.stack)
371
+ console.log('node', path.node)
372
+ return {
373
+ type: 'attr',
374
+ value: path.node,
375
+ } as const
376
+ }
377
+ })
378
+ .flat(4)
379
+ .filter(isPresent)
380
+
381
+ function isStaticAttributeName(name: string) {
382
+ return !!(
383
+ !!validStyles[name] ||
384
+ staticConfig.validPropsExtra?.[name] ||
385
+ !!pseudos[name] ||
386
+ staticConfig.variants?.[name] ||
387
+ tamaguiConfig.shorthands[name] ||
388
+ (name[0] === '$' ? !!mediaQueryConfig[name.slice(1)] : false)
389
+ )
390
+ }
391
+
392
+ function isExtractable(obj: t.Node): obj is t.ObjectExpression {
393
+ return (
394
+ t.isObjectExpression(obj) &&
395
+ obj.properties.every((prop) => {
396
+ if (!t.isObjectProperty(prop)) {
397
+ console.log('not object prop', prop)
398
+ return false
399
+ }
400
+ const propName = prop.key['name']
401
+ if (!isStaticAttributeName(propName) && propName !== 'tag') {
402
+ // if (shouldPrintDebug) {
403
+ console.log(' not a valid style prop!', propName)
404
+ // }
405
+ return false
406
+ }
407
+ return true
408
+ })
409
+ )
410
+ }
411
+
412
+ // side <= { color: 'red', background: x ? 'red' : 'green' }
413
+ // | => Ternary<test, { color: 'red' }, null>
414
+ // | => Ternary<test && x, { background: 'red' }, null>
415
+ // | => Ternary<test && !x, { background: 'green' }, null>
416
+ function createTernariesFromObjectProperties(
417
+ test: t.Expression,
418
+ side: t.Expression | null,
419
+ ternaryPartial: Partial<Ternary> = {}
420
+ ): null | Ternary[] {
421
+ if (!side) {
422
+ return null
423
+ }
424
+ if (!isExtractable(side)) {
425
+ throw new Error('not extractable')
426
+ }
427
+ return side.properties.flatMap((property) => {
428
+ if (!t.isObjectProperty(property)) {
429
+ throw new Error('expected object property')
430
+ }
431
+ // this could be a recurse here if we want to get fancy
432
+ if (t.isConditionalExpression(property.value)) {
433
+ // merge up into the parent conditional, split into two
434
+ const [truthy, falsy] = [
435
+ t.objectExpression([t.objectProperty(property.key, property.value.consequent)]),
436
+ t.objectExpression([t.objectProperty(property.key, property.value.alternate)]),
437
+ ].map((x) => attemptEval(x))
438
+ return [
439
+ createTernary({
440
+ remove() {},
441
+ ...ternaryPartial,
442
+ test: t.logicalExpression('&&', test, property.value.test),
443
+ consequent: truthy,
444
+ alternate: null,
445
+ }),
446
+ createTernary({
447
+ ...ternaryPartial,
448
+ test: t.logicalExpression(
449
+ '&&',
450
+ test,
451
+ t.unaryExpression('!', property.value.test)
452
+ ),
453
+ consequent: falsy,
454
+ alternate: null,
455
+ remove() {},
456
+ }),
457
+ ]
458
+ }
459
+ const obj = t.objectExpression([t.objectProperty(property.key, property.value)])
460
+ const consequent = attemptEval(obj)
461
+ return createTernary({
462
+ remove() {},
463
+ ...ternaryPartial,
464
+ test,
465
+ consequent,
466
+ alternate: null,
467
+ })
468
+ })
469
+ }
470
+
471
+ // START function evaluateAttribute
472
+ function evaluateAttribute(
473
+ path: NodePath<t.JSXAttribute | t.JSXSpreadAttribute>
474
+ ): ExtractedAttr | ExtractedAttr[] | null {
475
+ const attribute = path.node
476
+ const attr: ExtractedAttr = { type: 'attr', value: attribute }
477
+ // ...spreads
478
+ if (t.isJSXSpreadAttribute(attribute)) {
479
+ const arg = attribute.argument
480
+ const conditional = t.isConditionalExpression(arg)
481
+ ? // <YStack {...isSmall ? { color: 'red } : { color: 'blue }}
482
+ ([arg.test, arg.consequent, arg.alternate] as const)
483
+ : t.isLogicalExpression(arg) && arg.operator === '&&'
484
+ ? // <YStack {...isSmall && { color: 'red }}
485
+ ([arg.left, arg.right, null] as const)
486
+ : null
487
+
488
+ if (conditional) {
489
+ const [test, alt, cons] = conditional
490
+ if (!test) throw new Error(`no test`)
491
+ if ([alt, cons].some((side) => side && !isExtractable(side))) {
492
+ console.log('not extractable', alt, cons)
493
+ return attr
494
+ }
495
+ // split into individual ternaries per object property
496
+ return [
497
+ ...(createTernariesFromObjectProperties(test, alt) || []),
498
+ ...((cons &&
499
+ createTernariesFromObjectProperties(t.unaryExpression('!', test), cons)) ||
500
+ []),
501
+ ].map((ternary) => ({
502
+ type: 'ternary',
503
+ value: ternary,
504
+ }))
505
+ }
506
+ }
507
+ // END ...spreads
508
+
509
+ // directly keep these
510
+ // couldn't evaluate spread, undefined name, or name is not string
511
+ if (
512
+ t.isJSXSpreadAttribute(attribute) ||
513
+ !attribute.name ||
514
+ typeof attribute.name.name !== 'string'
515
+ ) {
516
+ inlinePropCount++
517
+ return attr
518
+ }
519
+
520
+ const name = attribute.name.name
521
+
522
+ if (isExcludedProp(name)) {
523
+ return null
524
+ }
525
+
526
+ // can still optimize the object... see hoverStyle on native
527
+ if (isDeoptedProp(name)) {
528
+ if (shouldPrintDebug) {
529
+ console.log(' ! inlining, deopt prop', name)
530
+ }
531
+ inlinePropCount++
532
+ return attr
533
+ }
534
+
535
+ // pass className, key, and style props through untouched
536
+ if (UNTOUCHED_PROPS[name]) {
537
+ return attr
538
+ }
539
+
540
+ // shorthand media queries
541
+ if (name[0] === '$' && t.isJSXExpressionContainer(attribute?.value)) {
542
+ const shortname = name.slice(1)
543
+ if (mediaQueryConfig[shortname]) {
544
+ const expression = attribute.value.expression
545
+ if (!t.isJSXEmptyExpression(expression)) {
546
+ const ternaries = createTernariesFromObjectProperties(
547
+ t.stringLiteral(shortname),
548
+ expression,
549
+ {
550
+ inlineMediaQuery: shortname,
551
+ }
552
+ )
553
+ if (ternaries) {
554
+ return ternaries.map((value) => ({
555
+ type: 'ternary',
556
+ value,
557
+ }))
558
+ }
559
+ }
560
+ }
561
+ }
562
+
563
+ const [value, valuePath] = (() => {
564
+ if (t.isJSXExpressionContainer(attribute?.value)) {
565
+ return [attribute.value.expression!, path.get('value')!] as const
566
+ } else {
567
+ return [attribute.value!, path.get('value')!] as const
568
+ }
569
+ })()
570
+
571
+ const remove = () => {
572
+ Array.isArray(valuePath) ? valuePath.map((p) => p.remove()) : valuePath.remove()
573
+ }
574
+
575
+ if (name === 'ref') {
576
+ if (shouldPrintDebug) {
577
+ console.log(' ! inlining, ref', name)
578
+ }
579
+ inlinePropCount++
580
+ return attr
581
+ }
582
+
583
+ if (name === 'tag') {
584
+ return {
585
+ type: 'attr',
586
+ value: path.node,
587
+ }
588
+ }
589
+
590
+ // if value can be evaluated, extract it and filter it out
591
+ const styleValue = attemptEvalSafe(value)
592
+
593
+ // we never flatten if a prop isn't a valid static attribute
594
+ // but we need to make sure its post-prop mapping
595
+ if (!isStaticAttributeName(name)) {
596
+ let keys = [name]
597
+ if (staticConfig.propMapper) {
598
+ // for now passing empty props {}, a bit odd, need to at least document
599
+ // for now we don't expose custom components so just noting behavior
600
+ const out = staticConfig.propMapper(
601
+ name,
602
+ styleValue,
603
+ defaultTheme,
604
+ staticConfig.defaultProps
605
+ )
606
+ if (out) {
607
+ keys = Object.keys(out)
608
+ }
609
+ }
610
+ if (keys.some((k) => !isStaticAttributeName(k) && k !== 'tag')) {
611
+ if (shouldPrintDebug) {
612
+ console.log(' ! inlining, not static attribute name', name)
613
+ }
614
+ inlinePropCount++
615
+ return attr
616
+ }
617
+ }
618
+
619
+ // FAILED = dynamic or ternary, keep going
620
+ if (styleValue !== FAILED_EVAL) {
621
+ return {
622
+ type: 'style',
623
+ value: { [name]: styleValue },
624
+ }
625
+ }
626
+
627
+ // ternaries!
628
+
629
+ // binary ternary, we can eventually make this smarter but step 1
630
+ // basically for the common use case of:
631
+ // opacity={(conditional ? 0 : 1) * scale}
632
+ if (t.isBinaryExpression(value)) {
633
+ const { operator, left, right } = value
634
+ // if one side is a ternary, and the other side is evaluatable, we can maybe extract
635
+ const lVal = attemptEvalSafe(left)
636
+ const rVal = attemptEvalSafe(right)
637
+ if (shouldPrintDebug) {
638
+ console.log(` evalBinaryExpression lVal ${String(lVal)}, rVal ${String(rVal)}`)
639
+ }
640
+ if (lVal !== FAILED_EVAL && t.isConditionalExpression(right)) {
641
+ const ternary = addBinaryConditional(operator, left, right)
642
+ if (ternary) return ternary
643
+ }
644
+ if (rVal !== FAILED_EVAL && t.isConditionalExpression(left)) {
645
+ const ternary = addBinaryConditional(operator, right, left)
646
+ if (ternary) return ternary
647
+ }
648
+ if (shouldPrintDebug) {
649
+ console.log(` evalBinaryExpression cant extract`)
650
+ }
651
+ inlinePropCount++
652
+ return attr
653
+ }
654
+
655
+ const staticConditional = getStaticConditional(value)
656
+ if (staticConditional) {
657
+ return { type: 'ternary', value: staticConditional }
658
+ }
659
+
660
+ const staticLogical = getStaticLogical(value)
661
+ if (staticLogical) {
662
+ return { type: 'ternary', value: staticLogical }
663
+ }
664
+
665
+ if (shouldPrintDebug) {
666
+ console.log(' ! inline prop via no match', name, value.type)
667
+ }
668
+ // if we've made it this far, the prop stays inline
669
+ inlinePropCount++
670
+
671
+ //
672
+ // RETURN ATTR
673
+ //
674
+ return attr
675
+
676
+ // attr helpers:
677
+ function addBinaryConditional(
678
+ operator: any,
679
+ staticExpr: any,
680
+ cond: t.ConditionalExpression
681
+ ): ExtractedAttr | null {
682
+ if (getStaticConditional(cond)) {
683
+ const alt = attemptEval(t.binaryExpression(operator, staticExpr, cond.alternate))
684
+ const cons = attemptEval(t.binaryExpression(operator, staticExpr, cond.consequent))
685
+ if (shouldPrintDebug) {
686
+ console.log(' binaryConditional', cond.test, cons, alt)
687
+ }
688
+ return {
689
+ type: 'ternary',
690
+ value: {
691
+ test: cond.test,
692
+ remove,
693
+ alternate: { [name]: alt },
694
+ consequent: { [name]: cons },
695
+ },
696
+ }
697
+ }
698
+ return null
699
+ }
700
+
701
+ function getStaticConditional(value: t.Node): Ternary | null {
702
+ if (t.isConditionalExpression(value)) {
703
+ try {
704
+ if (shouldPrintDebug) {
705
+ console.log('attempt', value.alternate, value.consequent)
706
+ }
707
+ const aVal = attemptEval(value.alternate)
708
+ const cVal = attemptEval(value.consequent)
709
+ if (shouldPrintDebug) {
710
+ const type = value.test.type
711
+ console.log(' static ternary', type, cVal, aVal)
712
+ }
713
+ return {
714
+ test: value.test,
715
+ remove,
716
+ consequent: { [name]: cVal },
717
+ alternate: { [name]: aVal },
718
+ }
719
+ } catch (err: any) {
720
+ if (shouldPrintDebug) {
721
+ console.log(' cant eval ternary', err.message)
722
+ }
723
+ }
724
+ }
725
+ return null
726
+ }
727
+
728
+ function getStaticLogical(value: t.Node): Ternary | null {
729
+ if (t.isLogicalExpression(value)) {
730
+ if (value.operator === '&&') {
731
+ try {
732
+ const val = attemptEval(value.right)
733
+ if (shouldPrintDebug) {
734
+ console.log(' staticLogical', value.left, name, val)
735
+ }
736
+ return {
737
+ test: value.left,
738
+ remove,
739
+ consequent: { [name]: val },
740
+ alternate: null,
741
+ }
742
+ } catch (err) {
743
+ if (shouldPrintDebug) {
744
+ console.log(' cant static eval logical', err)
745
+ }
746
+ }
747
+ }
748
+ }
749
+ return null
750
+ }
751
+ } // END function evaluateAttribute
752
+
753
+ // see if we can filter them
754
+ if (shouldPrintDebug) {
755
+ console.log(' - attrs (before): ', attrs.map(attrStr).join(', '))
756
+ }
757
+
758
+ // now update to new values
759
+ node.attributes = attrs.filter(isAttr).map((x) => x.value)
760
+
761
+ if (couldntParse) {
762
+ if (shouldPrintDebug) {
763
+ console.log(` cancel:`, { couldntParse, shouldDeopt })
764
+ }
765
+ node.attributes = ogAttributes
766
+ return
767
+ }
768
+
769
+ // before deopt, can still optimize
770
+ const parentFn = findTopmostFunction(traversePath)
771
+ if (parentFn) {
772
+ modifiedComponents.add(parentFn)
773
+ }
774
+
775
+ const filePath = sourcePath.replace(process.cwd(), '.')
776
+ const lineNumbers = node.loc
777
+ ? node.loc.start.line +
778
+ (node.loc.start.line !== node.loc.end.line ? `-${node.loc.end.line}` : '')
779
+ : ''
780
+
781
+ // combine ternaries
782
+ let ternaries: Ternary[] = []
783
+ attrs = attrs
784
+ .reduce<(ExtractedAttr | ExtractedAttr[])[]>((out, cur) => {
785
+ const next = attrs[attrs.indexOf(cur) + 1]
786
+ if (cur.type === 'ternary') {
787
+ ternaries.push(cur.value)
788
+ }
789
+ if ((!next || next.type !== 'ternary') && ternaries.length) {
790
+ // finish, process
791
+ const normalized = normalizeTernaries(ternaries).map(
792
+ ({ alternate, consequent, ...rest }) => {
793
+ return {
794
+ type: 'ternary' as const,
795
+ value: {
796
+ ...rest,
797
+ alternate: alternate || null,
798
+ consequent: consequent || null,
799
+ },
800
+ }
801
+ }
802
+ )
803
+ try {
804
+ return [...out, ...normalized]
805
+ } finally {
806
+ if (shouldPrintDebug) {
807
+ console.log(
808
+ ` normalizeTernaries (${ternaries.length} => ${normalized.length})`
809
+ )
810
+ }
811
+ ternaries = []
812
+ }
813
+ }
814
+ if (cur.type === 'ternary') {
815
+ return out
816
+ }
817
+ out.push(cur)
818
+ return out
819
+ }, [])
820
+ .flat()
821
+
822
+ // evaluates all static attributes into a simple object
823
+ const completeStaticProps = {
824
+ ...Object.keys(attrs).reduce((acc, index) => {
825
+ const cur = attrs[index] as ExtractedAttr
826
+ if (cur.type === 'style') {
827
+ Object.assign(acc, cur.value)
828
+ }
829
+ if (cur.type === 'attr') {
830
+ if (t.isJSXSpreadAttribute(cur.value)) {
831
+ return acc
832
+ }
833
+ if (!t.isJSXIdentifier(cur.value.name)) {
834
+ return acc
835
+ }
836
+ const key = cur.value.name.name
837
+ // undefined = boolean true
838
+ const value = attemptEvalSafe(cur.value.value || t.booleanLiteral(true))
839
+ if (value === FAILED_EVAL) {
840
+ return acc
841
+ }
842
+ acc[key] = value
843
+ }
844
+ return acc
845
+ }, {}),
846
+ }
847
+
848
+ // flatten logic!
849
+ // fairly simple check to see if all children are text
850
+ const hasSpread = node.attributes.some((x) => t.isJSXSpreadAttribute(x))
851
+ const hasOnlyStringChildren =
852
+ !hasSpread &&
853
+ (node.selfClosing ||
854
+ (traversePath.node.children &&
855
+ traversePath.node.children.every((x) => x.type === 'JSXText')))
856
+ const shouldFlatten =
857
+ !shouldDeopt &&
858
+ inlinePropCount === 0 &&
859
+ !hasSpread &&
860
+ staticConfig.neverFlatten !== true &&
861
+ (staticConfig.neverFlatten === 'jsx' ? hasOnlyStringChildren : true)
862
+
863
+ // insert overrides - this inserts null props for things that are set in classNames
864
+ // only when not flattening, so the downstream component can skip applying those styles
865
+ if (!shouldFlatten) {
866
+ attrs = attrs.reduce<ExtractedAttr[]>((acc, cur) => {
867
+ if (cur.type === 'style') {
868
+ // TODO need to loop over initial props not just style props
869
+ for (const key in cur.value) {
870
+ const shouldInsertNull =
871
+ !!(
872
+ staticConfig.ensureOverriddenProp?.[key] // || staticConfig.defaultProps?.[key]
873
+ )
874
+ const isSetInAttrsAlready = attrs.some(
875
+ (x) =>
876
+ x.type === 'attr' &&
877
+ x.value.type === 'JSXAttribute' &&
878
+ x.value.name.name === key
879
+ )
880
+ const shouldInsertNullOverride = shouldInsertNull && !isSetInAttrsAlready
881
+ // if (shouldPrintDebug) {
882
+ // // prettier-ignore
883
+ // console.log('what is', key, { shouldInsertNullOverride, shouldInsertNull, isSetInAttrsAlready })
884
+ // }
885
+ if (shouldInsertNullOverride) {
886
+ acc.push({
887
+ type: 'attr',
888
+ value: t.jsxAttribute(
889
+ t.jsxIdentifier(key),
890
+ t.jsxExpressionContainer(t.nullLiteral())
891
+ ),
892
+ })
893
+ }
894
+ }
895
+ }
896
+ acc.push(cur)
897
+ return acc
898
+ }, [])
899
+ }
900
+
901
+ if (shouldPrintDebug) {
902
+ console.log(' - attrs (flattened): ', attrs.map(attrStr).join(', '))
903
+ }
904
+
905
+ // evaluate away purely style props
906
+ attrs = attrs.reduce<ExtractedAttr[]>((acc, cur) => {
907
+ if (
908
+ cur.type !== 'attr' ||
909
+ !t.isJSXAttribute(cur.value) ||
910
+ typeof cur.value.name.name !== 'string'
911
+ ) {
912
+ if (cur.type === 'style') {
913
+ const key = Object.keys(cur.value)[0]
914
+ if (!validStyles[key] && !pseudos[key]) {
915
+ if (shouldPrintDebug) {
916
+ console.log(' ❌ excluding', key)
917
+ }
918
+ // we've already expanded shorthands, now we can remove them
919
+ return acc
920
+ }
921
+ }
922
+ }
923
+ acc.push(cur)
924
+ return acc
925
+ }, [])
926
+
927
+ if (shouldPrintDebug) {
928
+ console.log(' - attrs (evaluated styles): ', attrs.map(attrStr).join(', '))
929
+ }
930
+
931
+ // combine styles, leave undefined values
932
+ let prev: ExtractedAttr | null = null
933
+ attrs = attrs.reduce<ExtractedAttr[]>((acc, cur) => {
934
+ if (cur.type === 'style') {
935
+ if (prev?.type === 'style') {
936
+ Object.assign(prev.value, cur.value)
937
+ return acc
938
+ }
939
+ }
940
+ acc.push(cur)
941
+ prev = cur
942
+ return acc
943
+ }, [])
944
+
945
+ if (shouldPrintDebug) {
946
+ console.log(' - attrs (combined 🔀): ', attrs.map(attrStr).join(', '))
947
+ }
948
+
949
+ // post process
950
+ const getStyles = (props: Object | null) => {
951
+ if (!props) return
952
+ if (!!excludeProps.size) {
953
+ for (const key in props) {
954
+ if (isExcludedProp(key)) delete props[key]
955
+ }
956
+ }
957
+
958
+ const out = postProcessStyles(props, staticConfig, defaultTheme)
959
+ const next = out?.style ?? props
960
+ if (shouldPrintDebug) {
961
+ console.log(' >> getStyles: ', objToStr(props), '==>>', objToStr(next))
962
+ console.log(' >> style: ', objToStr(out.style))
963
+ console.log(' >> viewp: ', objToStr(out.viewProps))
964
+ }
965
+ if (staticConfig.validStyles) {
966
+ for (const key in next) {
967
+ if (!staticConfig.validStyles[key] && !pseudos[key]) {
968
+ delete next[key]
969
+ }
970
+ }
971
+ }
972
+ return next
973
+ }
974
+
975
+ if (shouldPrintDebug) {
976
+ console.log(' staticConfig.defaultProps', staticConfig.defaultProps)
977
+ }
978
+
979
+ // used to ensure we pass the entire prop bundle to getStyles
980
+ const completeStylesProcessed = getStyles({
981
+ ...staticConfig.defaultProps,
982
+ ...completeStaticProps,
983
+ })
984
+
985
+ // any extra styles added in postprocess should be added to first group as they wont be overriden
986
+ const stylesToAddToInitialGroup = difference(
987
+ Object.keys(completeStylesProcessed),
988
+ Object.keys(completeStaticProps)
989
+ )
990
+
991
+ if (stylesToAddToInitialGroup.length) {
992
+ const toAdd = pick(completeStylesProcessed, ...stylesToAddToInitialGroup)
993
+ const firstGroup = attrs.find((x) => x.type === 'style')
994
+ if (shouldPrintDebug) {
995
+ console.log(' stylesToAddToInitialGroup', stylesToAddToInitialGroup.join(', '))
996
+ console.log(' toAdd', objToStr(toAdd))
997
+ }
998
+ if (!firstGroup) {
999
+ attrs.unshift({ type: 'style', value: toAdd })
1000
+ } else {
1001
+ Object.assign(firstGroup.value, toAdd)
1002
+ }
1003
+ }
1004
+
1005
+ if (shouldPrintDebug) {
1006
+ // prettier-ignore
1007
+ console.log(' completeStaticProps', objToStr(completeStaticProps))
1008
+ // prettier-ignore
1009
+ console.log(' completeStylesProcessed', objToStr(completeStylesProcessed))
1010
+ }
1011
+
1012
+ for (const attr of attrs) {
1013
+ try {
1014
+ switch (attr.type) {
1015
+ case 'ternary':
1016
+ const a = getStyles(attr.value.alternate)
1017
+ const c = getStyles(attr.value.consequent)
1018
+ attr.value.alternate = a
1019
+ attr.value.consequent = c
1020
+ if (shouldPrintDebug) {
1021
+ console.log(' => tern ', attrStr(attr))
1022
+ }
1023
+ break
1024
+ case 'style':
1025
+ const next = {}
1026
+ for (const key in attr.value) {
1027
+ if (key in stylePropsTransform) {
1028
+ // TODO this logic needs to be a bit more right, because could have spread in between transforms...
1029
+ next['transform'] = completeStylesProcessed['transform']
1030
+ } else {
1031
+ next[key] = completeStylesProcessed[key] ?? attr.value[key]
1032
+ }
1033
+ }
1034
+ attr.value = next
1035
+ break
1036
+ }
1037
+ } catch (err) {
1038
+ // any error de-opt
1039
+ if (shouldPrintDebug) {
1040
+ console.log(' postprocessing error, deopt', err)
1041
+ node.attributes = ogAttributes
1042
+ return node
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ if (shouldPrintDebug) {
1048
+ console.log(' - attrs (after): ', attrs.map(attrStr).join(', '))
1049
+ }
1050
+
1051
+ // add data-is
1052
+ if (shouldAddDebugProp) {
1053
+ const preName = componentName ? `${componentName}:` : ''
1054
+ // unshift so spreads/nesting overwrite
1055
+ attrs.unshift({
1056
+ type: 'attr',
1057
+ value: t.jsxAttribute(
1058
+ t.jsxIdentifier('data-is'),
1059
+ t.stringLiteral(
1060
+ ` ${preName}${node.name.name} ${filePath.replace('./', '')}:${lineNumbers} `
1061
+ )
1062
+ ),
1063
+ })
1064
+ }
1065
+
1066
+ if (shouldFlatten) {
1067
+ // DO FLATTEN
1068
+ if (shouldPrintDebug) {
1069
+ console.log(' [✅] flattening', originalNodeName, flatNode)
1070
+ }
1071
+ isFlattened = true
1072
+ node.name.name = flatNode
1073
+ onDidFlatten?.()
1074
+ if (closingElement) {
1075
+ closingElement.name.name = flatNode
1076
+ }
1077
+ }
1078
+
1079
+ if (shouldPrintDebug) {
1080
+ // prettier-ignore
1081
+ console.log(' [❊] inline props ', inlinePropCount, shouldDeopt ? ' deopted' : '', hasSpread ? ' spread' : '', '!flatten', staticConfig.neverFlatten)
1082
+ console.log(' - attrs (end): ', attrs.map(attrStr).join(', '))
1083
+ }
1084
+
1085
+ onExtractTag({
1086
+ attrs,
1087
+ node,
1088
+ lineNumbers,
1089
+ filePath,
1090
+ attemptEval,
1091
+ jsxPath: traversePath,
1092
+ originalNodeName,
1093
+ isFlattened,
1094
+ programPath,
1095
+ })
1096
+ },
1097
+ })
1098
+
1099
+ /**
1100
+ * Step 3: Remove dead code from removed media query / theme hooks
1101
+ */
1102
+ if (modifiedComponents.size) {
1103
+ const all = Array.from(modifiedComponents)
1104
+ if (shouldPrintDebug) {
1105
+ console.log(' [🪝] hook check', all.length)
1106
+ }
1107
+ for (const comp of all) {
1108
+ removeUnusedHooks(comp, shouldPrintDebug)
1109
+ }
1110
+ }
1111
+ },
1112
+ }
1113
+ }