@tamagui/static 1.0.1-beta.56 → 1.0.1-beta.60

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 (41) 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/patchReactNativeWeb.js +1 -1
  4. package/dist/cjs/patchReactNativeWeb.js.map +2 -2
  5. package/dist/esm/extractor/createExtractor.js +18 -1
  6. package/dist/esm/extractor/createExtractor.js.map +2 -2
  7. package/dist/esm/patchReactNativeWeb.js +1 -1
  8. package/dist/esm/patchReactNativeWeb.js.map +2 -2
  9. package/dist/jsx/extractor/createExtractor.js +18 -1
  10. package/dist/jsx/patchReactNativeWeb.js +1 -1
  11. package/package.json +9 -7
  12. package/src/constants.ts +13 -0
  13. package/src/extractor/accessSafe.ts +18 -0
  14. package/src/extractor/babelParse.ts +27 -0
  15. package/src/extractor/buildClassName.ts +61 -0
  16. package/src/extractor/createEvaluator.ts +69 -0
  17. package/src/extractor/createExtractor.ts +1696 -0
  18. package/src/extractor/ensureImportingConcat.ts +39 -0
  19. package/src/extractor/evaluateAstNode.ts +121 -0
  20. package/src/extractor/extractHelpers.ts +119 -0
  21. package/src/extractor/extractMediaStyle.ts +190 -0
  22. package/src/extractor/extractToClassNames.ts +455 -0
  23. package/src/extractor/findTopmostFunction.ts +22 -0
  24. package/src/extractor/generatedUid.ts +43 -0
  25. package/src/extractor/getPrefixLogs.ts +6 -0
  26. package/src/extractor/getPropValueFromAttributes.ts +92 -0
  27. package/src/extractor/getSourceModule.ts +101 -0
  28. package/src/extractor/getStaticBindingsForScope.ts +183 -0
  29. package/src/extractor/hoistClassNames.ts +45 -0
  30. package/src/extractor/literalToAst.ts +84 -0
  31. package/src/extractor/loadTamagui.ts +139 -0
  32. package/src/extractor/logLines.ts +16 -0
  33. package/src/extractor/normalizeTernaries.ts +63 -0
  34. package/src/extractor/removeUnusedHooks.ts +76 -0
  35. package/src/extractor/timer.ts +18 -0
  36. package/src/extractor/validHTMLAttributes.ts +99 -0
  37. package/src/index.ts +9 -0
  38. package/src/patchReactNativeWeb.ts +165 -0
  39. package/src/types.ts +97 -0
  40. package/types/extractor/createExtractor.d.ts +14 -1
  41. package/types/extractor/createExtractor.d.ts.map +1 -1
@@ -0,0 +1,39 @@
1
+ import { NodePath } from '@babel/traverse'
2
+ import * as t from '@babel/types'
3
+
4
+ import { CONCAT_CLASSNAME_IMPORT } from '../constants'
5
+
6
+ const importConcatPkg = '@tamagui/helpers'
7
+
8
+ export function ensureImportingConcat(path: NodePath<t.Program>) {
9
+ const bodyPath = path.get('body')
10
+ const imported: NodePath<t.ImportDeclaration> | undefined = bodyPath.find(
11
+ (x) => x.isImportDeclaration() && x.node.source.value === importConcatPkg
12
+ ) as any
13
+ const importSpecifier = t.importSpecifier(
14
+ t.identifier(CONCAT_CLASSNAME_IMPORT),
15
+ t.identifier(CONCAT_CLASSNAME_IMPORT)
16
+ )
17
+
18
+ if (!imported) {
19
+ path.node.body.push(t.importDeclaration([importSpecifier], t.stringLiteral(importConcatPkg)))
20
+ return
21
+ }
22
+
23
+ const specifiers = imported.node.specifiers
24
+ const alreadyImported = specifiers.some(
25
+ (x) =>
26
+ t.isImportSpecifier(x) &&
27
+ t.isIdentifier(x.imported) &&
28
+ x.imported.name === CONCAT_CLASSNAME_IMPORT
29
+ )
30
+
31
+ if (!alreadyImported) {
32
+ specifiers.push(
33
+ t.importSpecifier(
34
+ t.identifier(CONCAT_CLASSNAME_IMPORT),
35
+ t.identifier(CONCAT_CLASSNAME_IMPORT)
36
+ )
37
+ )
38
+ }
39
+ }
@@ -0,0 +1,121 @@
1
+ import * as t from '@babel/types'
2
+
3
+ export function evaluateAstNode(
4
+ exprNode: t.Node | undefined | null,
5
+ evalFn?: (node: t.Node) => any,
6
+ shouldPrintDebug?: boolean | 'verbose'
7
+ ): any {
8
+ if (exprNode === undefined) {
9
+ return undefined
10
+ }
11
+
12
+ // null === boolean true (at least in our use cases for jsx eval)
13
+ if (exprNode === null) {
14
+ return true
15
+ }
16
+
17
+ if (t.isJSXExpressionContainer(exprNode)) {
18
+ return evaluateAstNode(exprNode.expression)
19
+ }
20
+
21
+ // loop through ObjectExpression keys
22
+ if (t.isObjectExpression(exprNode)) {
23
+ const ret: Record<string, any> = {}
24
+ for (let idx = -1, len = exprNode.properties.length; ++idx < len; ) {
25
+ const value = exprNode.properties[idx]
26
+
27
+ if (!t.isObjectProperty(value)) {
28
+ throw new Error('evaluateAstNode can only evaluate object properties')
29
+ }
30
+
31
+ let key: string | number | null | undefined | boolean
32
+ if (value.computed) {
33
+ if (typeof evalFn !== 'function') {
34
+ throw new Error(
35
+ 'evaluateAstNode does not support computed keys unless an eval function is provided'
36
+ )
37
+ }
38
+ key = evaluateAstNode(value.key, evalFn)
39
+ } else if (t.isIdentifier(value.key)) {
40
+ key = value.key.name
41
+ } else if (t.isStringLiteral(value.key) || t.isNumericLiteral(value.key)) {
42
+ key = value.key.value
43
+ } else {
44
+ throw new Error('Unsupported key type: ' + value.key.type)
45
+ }
46
+
47
+ if (typeof key !== 'string' && typeof key !== 'number') {
48
+ throw new Error('key must be either a string or a number')
49
+ }
50
+
51
+ ret[key] = evaluateAstNode(value.value, evalFn)
52
+ }
53
+ return ret
54
+ }
55
+
56
+ if (t.isArrayExpression(exprNode)) {
57
+ return exprNode.elements.map((x) => {
58
+ return evaluateAstNode(x as any, evalFn)
59
+ })
60
+ }
61
+
62
+ if (t.isUnaryExpression(exprNode) && exprNode.operator === '-') {
63
+ const ret = evaluateAstNode(exprNode.argument, evalFn)
64
+ if (ret == null) {
65
+ return null
66
+ }
67
+ return -ret
68
+ }
69
+
70
+ if (t.isTemplateLiteral(exprNode)) {
71
+ if (typeof evalFn !== 'function') {
72
+ throw new Error(
73
+ 'evaluateAstNode does not support template literals unless an eval function is provided'
74
+ )
75
+ }
76
+
77
+ let ret: string = ''
78
+ for (let idx = -1, len = exprNode.quasis.length; ++idx < len; ) {
79
+ const quasi = exprNode.quasis[idx]
80
+ const expr = exprNode.expressions[idx]
81
+ ret += quasi.value.raw
82
+ if (expr) {
83
+ ret += evaluateAstNode(expr, evalFn)
84
+ }
85
+ }
86
+ return ret
87
+ }
88
+
89
+ // In the interest of representing the "evaluated" prop
90
+ // as the user intended, we support negative null. Why not.
91
+ if (t.isNullLiteral(exprNode)) {
92
+ return null
93
+ }
94
+
95
+ if (t.isNumericLiteral(exprNode) || t.isStringLiteral(exprNode) || t.isBooleanLiteral(exprNode)) {
96
+ // In the interest of representing the "evaluated" prop
97
+ // as the user intended, we support negative null. Why not.
98
+ return exprNode.value
99
+ }
100
+
101
+ if (t.isBinaryExpression(exprNode)) {
102
+ if (exprNode.operator === '+') {
103
+ return evaluateAstNode(exprNode.left, evalFn) + evaluateAstNode(exprNode.right, evalFn)
104
+ } else if (exprNode.operator === '-') {
105
+ return evaluateAstNode(exprNode.left, evalFn) - evaluateAstNode(exprNode.right, evalFn)
106
+ } else if (exprNode.operator === '*') {
107
+ return evaluateAstNode(exprNode.left, evalFn) * evaluateAstNode(exprNode.right, evalFn)
108
+ } else if (exprNode.operator === '/') {
109
+ return evaluateAstNode(exprNode.left, evalFn) / evaluateAstNode(exprNode.right, evalFn)
110
+ }
111
+ }
112
+
113
+ // if we've made it this far, the value has to be evaluated
114
+ if (typeof evalFn !== 'function') {
115
+ throw new Error(
116
+ 'evaluateAstNode does not support non-literal values unless an eval function is provided'
117
+ )
118
+ }
119
+
120
+ return evalFn(exprNode)
121
+ }
@@ -0,0 +1,119 @@
1
+ import generate from '@babel/generator'
2
+ import type { NodePath } from '@babel/traverse'
3
+ import * as t from '@babel/types'
4
+
5
+ import { ExtractedAttr, Ternary } from '../types'
6
+ import { astToLiteral } from './literalToAst'
7
+
8
+ export function isPresent<T extends Object>(input: null | void | undefined | T): input is T {
9
+ return input != null
10
+ }
11
+
12
+ export function isSimpleSpread(node: t.JSXSpreadAttribute) {
13
+ return t.isIdentifier(node.argument) || t.isMemberExpression(node.argument)
14
+ }
15
+
16
+ export const attrStr = (attr?: ExtractedAttr) => {
17
+ return !attr
18
+ ? ''
19
+ : attr.type === 'attr'
20
+ ? getNameAttr(attr.value)
21
+ : attr.type === 'ternary'
22
+ ? `...${ternaryStr(attr.value)}`
23
+ : `${attr.type}(${objToStr(attr.value)})`
24
+ }
25
+
26
+ export const objToStr = (obj: any, spacer = ', ') => {
27
+ if (!obj) {
28
+ return `${obj}`
29
+ }
30
+ return `{${Object.entries(obj)
31
+ .map(
32
+ ([k, v]) =>
33
+ `${k}:${
34
+ Array.isArray(v)
35
+ ? `[...]`
36
+ : v && typeof v === 'object'
37
+ ? `${objToStr(v, ',')}`
38
+ : JSON.stringify(v)
39
+ }`
40
+ )
41
+ .join(spacer)}}`
42
+ }
43
+
44
+ const getNameAttr = (attr: t.JSXAttribute | t.JSXSpreadAttribute) => {
45
+ if (t.isJSXSpreadAttribute(attr)) {
46
+ return `...${attr.argument['name']}`
47
+ }
48
+ return 'name' in attr ? attr.name.name : `unknown-${attr['type']}`
49
+ }
50
+
51
+ export const ternaryStr = (x: Ternary) => {
52
+ const conditional = t.isIdentifier(x.test)
53
+ ? x.test.name
54
+ : t.isMemberExpression(x.test)
55
+ ? [x.test.object['name'], x.test.property['name']]
56
+ : generate(x.test).code
57
+ return [
58
+ 'ternary(',
59
+ conditional,
60
+ isFilledObj(x.consequent) ? ` ? ${objToStr(x.consequent)}` : ' ? 🚫',
61
+ isFilledObj(x.alternate) ? ` : ${objToStr(x.alternate)}` : ' : 🚫',
62
+ ')',
63
+ ]
64
+ .flat()
65
+ .join('')
66
+ }
67
+
68
+ const isFilledObj = (obj: any) => obj && Object.keys(obj).length
69
+
70
+ export function findComponentName(scope) {
71
+ let componentName = ''
72
+ let cur = scope.path
73
+ while (cur.parentPath && !t.isProgram(cur.parentPath.parent)) {
74
+ cur = cur.parentPath
75
+ }
76
+ let node = cur.parent
77
+ if (t.isExportNamedDeclaration(node)) {
78
+ node = node.declaration
79
+ }
80
+ if (t.isVariableDeclaration(node)) {
81
+ const [dec] = node.declarations
82
+ if (t.isVariableDeclarator(dec) && t.isIdentifier(dec.id)) {
83
+ return dec.id.name
84
+ }
85
+ }
86
+ if (t.isFunctionDeclaration(node)) {
87
+ return node.id?.name
88
+ }
89
+ return componentName
90
+ }
91
+
92
+ export function isValidThemeHook(
93
+ jsxPath: NodePath<t.JSXElement>,
94
+ n: t.MemberExpression,
95
+ sourcePath: string
96
+ ) {
97
+ if (!t.isIdentifier(n.object) || !t.isIdentifier(n.property)) return false
98
+ const bindings = jsxPath.scope.getAllBindings()
99
+ const binding = bindings[n.object.name]
100
+ if (!binding?.path) return false
101
+ if (!binding.path.isVariableDeclarator()) return false
102
+ const init = binding.path.node.init
103
+ if (!t.isCallExpression(init)) return false
104
+ if (!t.isIdentifier(init.callee)) return false
105
+ // TODO could support renaming useTheme by looking up import first
106
+ if (init.callee.name !== 'useTheme') return false
107
+ const importNode = binding.scope.getBinding('useTheme')?.path.parent
108
+ if (!t.isImportDeclaration(importNode)) return false
109
+ if (importNode.source.value !== 'tamagui') {
110
+ if (!isInsideTamagui(sourcePath)) {
111
+ return false
112
+ }
113
+ }
114
+ return true
115
+ }
116
+
117
+ export const isInsideTamagui = (srcName: string) => {
118
+ return srcName.includes('/dist/jsx') || srcName.includes('/core/src')
119
+ }
@@ -0,0 +1,190 @@
1
+ import { NodePath } from '@babel/traverse'
2
+ import * as t from '@babel/types'
3
+ import { TamaguiInternalConfig, getStylesAtomic, mediaObjectToString } from '@tamagui/core-node'
4
+ import { ViewStyle } from 'react-native'
5
+
6
+ import { MEDIA_SEP } from '../constants'
7
+ import { StyleObject, Ternary } from '../types'
8
+ import { isInsideTamagui, isPresent } from './extractHelpers'
9
+
10
+ export function extractMediaStyle(
11
+ ternary: Ternary,
12
+ jsxPath: NodePath<t.JSXElement>,
13
+ tamaguiConfig: TamaguiInternalConfig,
14
+ sourcePath: string,
15
+ importance = 0,
16
+ shouldPrintDebug: boolean | 'verbose' = false
17
+ ) {
18
+ const mt = getMediaQueryTernary(ternary, jsxPath, sourcePath)
19
+ if (!mt) {
20
+ return null
21
+ }
22
+ const { key } = mt
23
+ const mq = tamaguiConfig.media[key]
24
+ if (!mq) {
25
+ console.error(`Media query "${key}" not found: ${Object.keys(tamaguiConfig.media)}`)
26
+ return null
27
+ }
28
+ const getStyleObj = (styleObj: ViewStyle | null, negate = false) => {
29
+ return styleObj ? { styleObj, negate } : null
30
+ }
31
+ const styleOpts = [
32
+ getStyleObj(ternary.consequent, false),
33
+ getStyleObj(ternary.alternate, true),
34
+ ].filter(isPresent)
35
+ if (shouldPrintDebug && !styleOpts.length) {
36
+ console.log(' media query, no styles?')
37
+ return null
38
+ }
39
+ // for now order first strongest
40
+ const mediaKeys = Object.keys(tamaguiConfig.media)
41
+ const mediaKeyPrecendence = mediaKeys.reduce((acc, cur, i) => {
42
+ acc[cur] = new Array(importance + 1).fill(':root').join('')
43
+ return acc
44
+ }, {})
45
+ let mediaStyles: StyleObject[] = []
46
+ for (const { styleObj, negate } of styleOpts) {
47
+ const styles = getStylesAtomic(styleObj)
48
+ const singleMediaStyles = styles.map((style) => {
49
+ const negKey = negate ? '0' : ''
50
+ const ogPrefix = style.identifier.slice(0, style.identifier.indexOf('-') + 1)
51
+ // adds an extra separator before and after to detect later in concatClassName
52
+ // so it goes from: "_f-[hash]"
53
+ // to: "_f-_sm0_[hash]" or "_f-_sm_[hash]"
54
+ const identifier = `${style.identifier.replace(
55
+ ogPrefix,
56
+ `${ogPrefix}${MEDIA_SEP}${key}${negKey}${MEDIA_SEP}`
57
+ )}`
58
+ const className = `.${identifier}`
59
+ const mediaSelector = mediaObjectToString(tamaguiConfig.media[key])
60
+ const screenStr = negate ? 'not all' : 'screen'
61
+ const mediaQuery = `${screenStr} and ${mediaSelector}`
62
+ const precendencePrefix = mediaKeyPrecendence[key]
63
+ const styleInner = style.rules[0].replace(style.identifier, identifier)
64
+ // combines media queries if they already exist
65
+ let styleRule = ''
66
+ if (styleInner.includes('@media')) {
67
+ // combine
68
+ styleRule = styleInner.replace('{', ` and ${mediaQuery} {`)
69
+ } else {
70
+ styleRule = `@media ${mediaQuery} { ${precendencePrefix} ${styleInner} }`
71
+ }
72
+ return {
73
+ ...style,
74
+ identifier,
75
+ className,
76
+ rules: [styleRule],
77
+ }
78
+ })
79
+ if (shouldPrintDebug === 'verbose') {
80
+ // prettier-ignore
81
+ console.log(' media styles:', importance, styleObj, singleMediaStyles.map(x => x.identifier).join(', '))
82
+ }
83
+ // add to output
84
+ mediaStyles = [...mediaStyles, ...singleMediaStyles]
85
+ }
86
+ // filter out
87
+ ternary.remove()
88
+ return { mediaStyles, ternaryWithoutMedia: mt.ternaryWithoutMedia }
89
+ }
90
+
91
+ function getMediaQueryTernary(
92
+ ternary: Ternary,
93
+ jsxPath: NodePath<t.JSXElement>,
94
+ sourcePath: string
95
+ ): null | {
96
+ key: string
97
+ bindingName: string
98
+ ternaryWithoutMedia: Ternary | null
99
+ } {
100
+ // this handles unwrapping logical && media query ternarys
101
+ // first, unwrap if it has media logicalExpression
102
+ if (t.isLogicalExpression(ternary.test) && ternary.test.operator === '&&') {
103
+ // *should* be normalized to always be on left side
104
+ const mediaLeft = getMediaInfoFromExpression(
105
+ ternary.test.left,
106
+ jsxPath,
107
+ sourcePath,
108
+ ternary.inlineMediaQuery
109
+ )
110
+ if (mediaLeft) {
111
+ return {
112
+ ...mediaLeft,
113
+ ternaryWithoutMedia: {
114
+ ...ternary,
115
+ test: ternary.test.right,
116
+ },
117
+ }
118
+ }
119
+ }
120
+ // const media = useMedia()
121
+ // ... media.sm
122
+ const result = getMediaInfoFromExpression(
123
+ ternary.test,
124
+ jsxPath,
125
+ sourcePath,
126
+ ternary.inlineMediaQuery
127
+ )
128
+ if (result) {
129
+ return {
130
+ ...result,
131
+ ternaryWithoutMedia: null,
132
+ }
133
+ }
134
+ return null
135
+ }
136
+
137
+ function getMediaInfoFromExpression(
138
+ test: t.Expression,
139
+ jsxPath: NodePath<t.JSXElement>,
140
+ sourcePath: string,
141
+ inlineMediaQuery?: string
142
+ ) {
143
+ if (inlineMediaQuery) {
144
+ return {
145
+ key: inlineMediaQuery,
146
+ bindingName: inlineMediaQuery,
147
+ }
148
+ }
149
+ if (t.isMemberExpression(test) && t.isIdentifier(test.object) && t.isIdentifier(test.property)) {
150
+ const name = test.object['name']
151
+ const key = test.property['name']
152
+ const bindings = jsxPath.scope.getAllBindings()
153
+ const binding = bindings[name]
154
+ if (!binding) return false
155
+ const bindingNode = binding.path?.node
156
+ if (!t.isVariableDeclarator(bindingNode) || !bindingNode.init) return false
157
+ if (!isValidMediaCall(jsxPath, bindingNode.init, sourcePath)) return false
158
+ return { key, bindingName: name }
159
+ }
160
+ if (t.isIdentifier(test)) {
161
+ const key = test.name
162
+ const node = jsxPath.scope.getBinding(test.name)?.path?.node
163
+ if (!t.isVariableDeclarator(node)) return false
164
+ if (!node.init || !isValidMediaCall(jsxPath, node.init, sourcePath)) return false
165
+ return { key, bindingName: key }
166
+ }
167
+ return null
168
+ }
169
+
170
+ export function isValidMediaCall(
171
+ jsxPath: NodePath<t.JSXElement>,
172
+ init: t.Expression,
173
+ sourcePath: string
174
+ ) {
175
+ if (!t.isCallExpression(init)) return false
176
+ if (!t.isIdentifier(init.callee)) return false
177
+ // TODO could support renaming useMedia by looking up import first
178
+ if (init.callee.name !== 'useMedia') return false
179
+ const bindings = jsxPath.scope.getAllBindings()
180
+ const mediaBinding = bindings['useMedia']
181
+ if (!mediaBinding) return false
182
+ const useMediaImport = mediaBinding.path.parent
183
+ if (!t.isImportDeclaration(useMediaImport)) return false
184
+ if (useMediaImport.source.value !== 'tamagui') {
185
+ if (!isInsideTamagui(sourcePath)) {
186
+ return false
187
+ }
188
+ }
189
+ return true
190
+ }