@tamagui/static 1.0.0-alpha.20 → 1.0.0-alpha.24

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