@tamagui/static 1.0.1-beta.59 → 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 (36) hide show
  1. package/dist/cjs/extractor/createExtractor.js +18 -1
  2. package/dist/cjs/extractor/createExtractor.js.map +2 -2
  3. package/dist/esm/extractor/createExtractor.js +18 -1
  4. package/dist/esm/extractor/createExtractor.js.map +2 -2
  5. package/dist/jsx/extractor/createExtractor.js +18 -1
  6. package/package.json +9 -7
  7. package/src/constants.ts +13 -0
  8. package/src/extractor/accessSafe.ts +18 -0
  9. package/src/extractor/babelParse.ts +27 -0
  10. package/src/extractor/buildClassName.ts +61 -0
  11. package/src/extractor/createEvaluator.ts +69 -0
  12. package/src/extractor/createExtractor.ts +1696 -0
  13. package/src/extractor/ensureImportingConcat.ts +39 -0
  14. package/src/extractor/evaluateAstNode.ts +121 -0
  15. package/src/extractor/extractHelpers.ts +119 -0
  16. package/src/extractor/extractMediaStyle.ts +190 -0
  17. package/src/extractor/extractToClassNames.ts +455 -0
  18. package/src/extractor/findTopmostFunction.ts +22 -0
  19. package/src/extractor/generatedUid.ts +43 -0
  20. package/src/extractor/getPrefixLogs.ts +6 -0
  21. package/src/extractor/getPropValueFromAttributes.ts +92 -0
  22. package/src/extractor/getSourceModule.ts +101 -0
  23. package/src/extractor/getStaticBindingsForScope.ts +183 -0
  24. package/src/extractor/hoistClassNames.ts +45 -0
  25. package/src/extractor/literalToAst.ts +84 -0
  26. package/src/extractor/loadTamagui.ts +139 -0
  27. package/src/extractor/logLines.ts +16 -0
  28. package/src/extractor/normalizeTernaries.ts +63 -0
  29. package/src/extractor/removeUnusedHooks.ts +76 -0
  30. package/src/extractor/timer.ts +18 -0
  31. package/src/extractor/validHTMLAttributes.ts +99 -0
  32. package/src/index.ts +9 -0
  33. package/src/patchReactNativeWeb.ts +165 -0
  34. package/src/types.ts +97 -0
  35. package/types/extractor/createExtractor.d.ts +14 -1
  36. package/types/extractor/createExtractor.d.ts.map +1 -1
@@ -0,0 +1,101 @@
1
+ import * as t from '@babel/types'
2
+
3
+ export interface SourceModule {
4
+ sourceModule?: string
5
+ imported?: string
6
+ local?: string
7
+ destructured?: boolean
8
+ usesImportSyntax: boolean
9
+ }
10
+
11
+ export function getSourceModule(
12
+ itemName: string,
13
+ itemBinding: {
14
+ constant?: boolean
15
+ path: { node: t.Node; parent: any }
16
+ }
17
+ ): SourceModule | null {
18
+ // TODO: deal with reassignment
19
+ if (!itemBinding.constant) {
20
+ return null
21
+ }
22
+
23
+ let sourceModule: string | undefined
24
+ let imported: string | undefined
25
+ let local: string | undefined
26
+ let destructured: boolean | undefined
27
+ let usesImportSyntax = false
28
+
29
+ const itemNode = itemBinding.path.node
30
+
31
+ if (
32
+ // import x from 'y';
33
+ t.isImportDefaultSpecifier(itemNode) ||
34
+ // import {x} from 'y';
35
+ t.isImportSpecifier(itemNode)
36
+ ) {
37
+ if (t.isImportDeclaration(itemBinding.path.parent)) {
38
+ sourceModule = itemBinding.path.parent.source.value
39
+ local = itemNode.local.name
40
+ usesImportSyntax = true
41
+ if (t.isImportSpecifier(itemNode)) {
42
+ imported = itemNode.imported['name']
43
+ destructured = true
44
+ } else {
45
+ imported = itemNode.local.name
46
+ destructured = false
47
+ }
48
+ }
49
+ } else if (
50
+ t.isVariableDeclarator(itemNode) &&
51
+ itemNode.init != null &&
52
+ t.isCallExpression(itemNode.init) &&
53
+ t.isIdentifier(itemNode.init.callee) &&
54
+ itemNode.init.callee.name === 'require' &&
55
+ itemNode.init.arguments.length === 1
56
+ ) {
57
+ const firstArg = itemNode.init.arguments[0]
58
+ if (!t.isStringLiteral(firstArg)) {
59
+ return null
60
+ }
61
+ sourceModule = firstArg.value
62
+
63
+ if (t.isIdentifier(itemNode.id)) {
64
+ local = itemNode.id.name
65
+ imported = itemNode.id.name
66
+ destructured = false
67
+ } else if (t.isObjectPattern(itemNode.id)) {
68
+ for (const objProp of itemNode.id.properties) {
69
+ if (
70
+ t.isObjectProperty(objProp) &&
71
+ t.isIdentifier(objProp.value) &&
72
+ objProp.value.name === itemName
73
+ ) {
74
+ local = objProp.value.name
75
+ // @ts-ignore TODO remove this is only an issue on CI
76
+ imported = objProp.key.name
77
+ destructured = true
78
+ break
79
+ }
80
+ }
81
+
82
+ if (!local || !imported) {
83
+ console.error('could not find prop with value `%s`', itemName)
84
+ return null
85
+ }
86
+ } else {
87
+ console.error('Unhandled id type: %s', itemNode.id.type)
88
+ return null
89
+ }
90
+ } else {
91
+ return null
92
+ }
93
+
94
+ return {
95
+ destructured,
96
+ imported,
97
+ local,
98
+ sourceModule,
99
+ usesImportSyntax,
100
+ }
101
+ }
@@ -0,0 +1,183 @@
1
+ import { dirname, extname, resolve } from 'path'
2
+
3
+ import { Binding, NodePath } from '@babel/traverse'
4
+ import * as t from '@babel/types'
5
+ import { existsSync } from 'fs-extra'
6
+
7
+ import { evaluateAstNode } from './evaluateAstNode'
8
+ import { getSourceModule } from './getSourceModule'
9
+
10
+ const isLocalImport = (path: string) => path.startsWith('.') || path.startsWith('/')
11
+
12
+ function resolveImportPath(sourcePath: string, path: string) {
13
+ const sourceDir = dirname(sourcePath)
14
+ if (isLocalImport(path)) {
15
+ if (extname(path) === '') {
16
+ path += '.js'
17
+ }
18
+ return resolve(sourceDir, path)
19
+ }
20
+ return path
21
+ }
22
+
23
+ function importModule(path: string) {
24
+ const filenames = [path.replace('.js', '.tsx'), path.replace('.js', '.ts'), path]
25
+ for (const file of filenames) {
26
+ if (existsSync(file)) {
27
+ const { unregister } = require('esbuild-register/dist/node').register({
28
+ target: 'es2019',
29
+ format: 'cjs',
30
+ })
31
+ try {
32
+ // TODO we can clear this when we see updates on it later on
33
+ return require(file)
34
+ } catch {
35
+ // doesn't exists
36
+ } finally {
37
+ unregister()
38
+ }
39
+ }
40
+ }
41
+ return null
42
+ }
43
+
44
+ export function getStaticBindingsForScope(
45
+ scope: NodePath<t.JSXElement>['scope'],
46
+ whitelist: string[] = [],
47
+ sourcePath: string,
48
+ bindingCache: Record<string, string | null>,
49
+ shouldPrintDebug: boolean | 'verbose'
50
+ ): Record<string, any> {
51
+ const bindings: Record<string, Binding> = scope.getAllBindings() as any
52
+ const ret: Record<string, any> = {}
53
+
54
+ if (shouldPrintDebug) {
55
+ // prettier-ignore
56
+ console.log(' ', Object.keys(bindings).length, 'variables in scope')
57
+ // .map(x => bindings[x].identifier?.name).join(', ')
58
+ }
59
+
60
+ // on react native at least it doesnt find some bindings? not sure why
61
+ // lets add in whitelisted imports if they exist
62
+ const program = scope.getProgramParent().block as t.Program
63
+ for (const node of program.body) {
64
+ if (t.isImportDeclaration(node)) {
65
+ const importPath = node.source.value
66
+ if (!node.specifiers.length) continue
67
+ if (!isLocalImport(importPath)) {
68
+ continue
69
+ }
70
+ const moduleName = resolveImportPath(sourcePath, importPath)
71
+ const isOnWhitelist = whitelist.some((test) => moduleName.endsWith(test))
72
+ if (!isOnWhitelist) continue
73
+ const src = importModule(moduleName)
74
+ if (!src) continue
75
+ for (const specifier of node.specifiers) {
76
+ if (t.isImportSpecifier(specifier) && t.isIdentifier(specifier.imported)) {
77
+ if (typeof src[specifier.imported.name] !== 'undefined') {
78
+ const val = src[specifier.local.name]
79
+ ret[specifier.local.name] = val
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ if (!bindingCache) {
87
+ throw new Error('bindingCache is a required param')
88
+ }
89
+
90
+ for (const k in bindings) {
91
+ const binding = bindings[k]
92
+
93
+ // check to see if the item is a module
94
+ const sourceModule = getSourceModule(k, binding)
95
+ if (sourceModule) {
96
+ if (!sourceModule.sourceModule) {
97
+ continue
98
+ }
99
+
100
+ const moduleName = resolveImportPath(sourcePath, sourceModule.sourceModule)
101
+ const isOnWhitelist = whitelist.some((test) => moduleName.endsWith(test))
102
+
103
+ // TODO we could cache this at the file level.. and check if its been touched since
104
+
105
+ if (isOnWhitelist) {
106
+ const src = importModule(moduleName)
107
+ if (!src) {
108
+ console.log(
109
+ `⚠️ missing file ${moduleName} via ${sourcePath} import ${sourceModule.sourceModule}?`
110
+ )
111
+ return {}
112
+ }
113
+ if (sourceModule.destructured) {
114
+ if (sourceModule.imported) {
115
+ ret[k] = src[sourceModule.imported]
116
+ }
117
+ } else {
118
+ // crude esmodule check
119
+ // TODO: make sure this actually works
120
+ // if (src && src.__esModule) {
121
+ // ret[k] = src.default
122
+ // } else {
123
+ ret[k] = src
124
+ // }
125
+ }
126
+ }
127
+ continue
128
+ }
129
+
130
+ const { parent, parentPath } = binding.path
131
+
132
+ if (!t.isVariableDeclaration(parent) || parent.kind !== 'const') {
133
+ continue
134
+ }
135
+
136
+ // pick out the right variable declarator
137
+ const dec = parent.declarations.find((d) => t.isIdentifier(d.id) && d.id.name === k)
138
+
139
+ // if init is not set, there's nothing to evaluate
140
+ // TODO: handle spread syntax
141
+ if (!dec || !dec.init) {
142
+ continue
143
+ }
144
+
145
+ // missing start/end will break caching
146
+ if (typeof dec.id.start !== 'number' || typeof dec.id.end !== 'number') {
147
+ console.error('dec.id.start/end is not a number')
148
+ continue
149
+ }
150
+
151
+ if (!t.isIdentifier(dec.id)) {
152
+ console.error('dec is not an identifier')
153
+ continue
154
+ }
155
+
156
+ const cacheKey = `${dec.id.name}_${dec.id.start}-${dec.id.end}`
157
+
158
+ // retrieve value from cache
159
+ if (bindingCache.hasOwnProperty(cacheKey)) {
160
+ ret[k] = bindingCache[cacheKey]
161
+ continue
162
+ }
163
+ // retrieve value from cache
164
+ if (bindingCache.hasOwnProperty(cacheKey)) {
165
+ ret[k] = bindingCache[cacheKey]
166
+ continue
167
+ }
168
+
169
+ // evaluate
170
+ try {
171
+ ret[k] = evaluateAstNode(dec.init, undefined, shouldPrintDebug)
172
+ bindingCache[cacheKey] = ret[k]
173
+ continue
174
+ } catch (e) {
175
+ // skip
176
+ // if (shouldPrintDebug) {
177
+ // console.error('[🐇] cant eval, skipping', cacheKey) //, e.message)
178
+ // }
179
+ }
180
+ }
181
+
182
+ return ret
183
+ }
@@ -0,0 +1,45 @@
1
+ import { NodePath } from '@babel/traverse'
2
+ import * as t from '@babel/types'
3
+
4
+ export function hoistClassNames(
5
+ path: NodePath<t.JSXElement>,
6
+ existing: { [key: string]: t.Identifier },
7
+ expr: t.Expression
8
+ ) {
9
+ const hoist = hoistClassNames.bind(null, path, existing)
10
+ if (t.isStringLiteral(expr)) {
11
+ if (expr.value.trim() === '') {
12
+ return expr
13
+ }
14
+ if (existing[expr.value]) {
15
+ return existing[expr.value]
16
+ }
17
+ const identifier = replaceStringWithVariable(expr)
18
+ existing[expr.value] = identifier
19
+ return identifier
20
+ }
21
+ if (t.isBinaryExpression(expr)) {
22
+ return t.binaryExpression(expr.operator, hoist(expr.left), hoist(expr.right))
23
+ }
24
+ if (t.isLogicalExpression(expr)) {
25
+ return t.logicalExpression(expr.operator, hoist(expr.left), hoist(expr.right))
26
+ }
27
+ if (t.isConditionalExpression(expr)) {
28
+ return t.conditionalExpression(expr.test, hoist(expr.consequent), hoist(expr.alternate))
29
+ }
30
+ return expr
31
+
32
+ function replaceStringWithVariable(str: t.StringLiteral): t.Identifier {
33
+ // hoist outside fn!
34
+ const uid = path.scope.generateUidIdentifier('cn')
35
+ const parent = path.findParent((path) => path.isProgram())
36
+ if (!parent) throw new Error(`no program?`)
37
+ const variable = t.variableDeclaration('const', [
38
+ // adding a space for extra safety
39
+ t.variableDeclarator(uid, t.stringLiteral(` ${str.value}`)),
40
+ ])
41
+ // @ts-ignore
42
+ parent.unshiftContainer('body', variable)
43
+ return uid
44
+ }
45
+ }
@@ -0,0 +1,84 @@
1
+ import * as t from '@babel/types'
2
+
3
+ export function literalToAst(literal: any): t.Expression {
4
+ if (literal === null) {
5
+ return t.nullLiteral()
6
+ }
7
+ switch (typeof literal) {
8
+ case 'function':
9
+ throw new Error('Unsupported')
10
+ case 'number':
11
+ return t.numericLiteral(literal)
12
+ case 'string':
13
+ return t.stringLiteral(literal)
14
+ case 'boolean':
15
+ return t.booleanLiteral(literal)
16
+ case 'undefined':
17
+ return t.unaryExpression('void', t.numericLiteral(0), true)
18
+ default:
19
+ if (Array.isArray(literal)) {
20
+ return t.arrayExpression(literal.map(literalToAst))
21
+ }
22
+ return t.objectExpression(
23
+ Object.keys(literal)
24
+ .filter((k) => {
25
+ return typeof literal[k] !== 'undefined'
26
+ })
27
+ .map((k) => {
28
+ return t.objectProperty(t.stringLiteral(k), literalToAst(literal[k]))
29
+ })
30
+ )
31
+ }
32
+ }
33
+
34
+ const easyPeasies = ['BooleanLiteral', 'StringLiteral', 'NumericLiteral']
35
+
36
+ export function astToLiteral(node: any) {
37
+ if (!node) {
38
+ return
39
+ }
40
+ if (easyPeasies.includes(node.type)) {
41
+ return node.value
42
+ }
43
+ if (node.name === 'undefined' && !node.value) {
44
+ return undefined
45
+ }
46
+ if (t.isNullLiteral(node)) {
47
+ return null
48
+ }
49
+ if (t.isObjectExpression(node)) {
50
+ return computeProps(node.properties)
51
+ }
52
+ if (t.isArrayExpression(node)) {
53
+ return node.elements.reduce(
54
+ // @ts-ignore
55
+ (acc, element) => [
56
+ ...acc,
57
+ ...(element?.type === 'SpreadElement'
58
+ ? astToLiteral(element.argument)
59
+ : [astToLiteral(element)]),
60
+ ],
61
+ []
62
+ )
63
+ }
64
+ }
65
+
66
+ function computeProps(props) {
67
+ return props.reduce((acc, prop) => {
68
+ if (prop.type === 'SpreadElement') {
69
+ return {
70
+ ...acc,
71
+ ...astToLiteral(prop.argument),
72
+ }
73
+ } else if (prop.type !== 'ObjectMethod') {
74
+ const val = astToLiteral(prop.value)
75
+ if (val !== undefined) {
76
+ return {
77
+ ...acc,
78
+ [prop.key.name]: val,
79
+ }
80
+ }
81
+ }
82
+ return acc
83
+ }, {})
84
+ }
@@ -0,0 +1,139 @@
1
+ import { join } from 'path'
2
+
3
+ import type { TamaguiComponent, TamaguiInternalConfig } from '@tamagui/core'
4
+ import { createTamagui } from '@tamagui/core-node'
5
+
6
+ let loadedTamagui: any = null
7
+
8
+ export function loadTamagui(props: { components: string[]; config: string }): {
9
+ components: Record<string, TamaguiComponent>
10
+ tamaguiConfig: TamaguiInternalConfig
11
+ } {
12
+ if (loadedTamagui) {
13
+ return loadedTamagui
14
+ }
15
+
16
+ const configPath = join(process.cwd(), props.config)
17
+
18
+ // threaded caching avoiding 1s loading of large configs every save
19
+ // const cachePath = join(cacheDir, 'tamagui-conf-cached.json')
20
+ // const confStat = statSync(configPath)
21
+
22
+ // // TODO may want to disable, its pretty optimistic at caching...
23
+ // try {
24
+ // const confCache = readFileSync(cachePath, 'utf-8')
25
+ // const confParsed = JSON.parse(confCache)
26
+ // if (confParsed && confParsed.mtime === confStat.mtime) {
27
+ // return confParsed.value
28
+ // }
29
+ // } catch {
30
+ // // ok, no cache
31
+ // }
32
+
33
+ const x = Math.random()
34
+ const { unregister } = require('esbuild-register/dist/node').register({
35
+ target: 'es2019',
36
+ format: 'cjs',
37
+ })
38
+
39
+ try {
40
+ // lets shim require and avoid importing react-native + react-native-web
41
+ // we just need to read the config around them
42
+ process.env.IS_STATIC = 'is_static'
43
+ // @ts-ignore
44
+ if (typeof globalThis['__DEV__'] === 'undefined') {
45
+ // @ts-ignore
46
+ globalThis['__DEV__'] = process.env.NODE_ENV === 'development'
47
+ }
48
+
49
+ const proxyWorm = require('@tamagui/proxy-worm')
50
+ const rnw = require('react-native-web')
51
+ const Mod = require('module')
52
+ const og = Mod.prototype.require
53
+ Mod.prototype.require = function (path: string) {
54
+ if (path.endsWith('.css')) {
55
+ return {}
56
+ }
57
+ if (
58
+ path === '@gorhom/bottom-sheet' ||
59
+ path.startsWith('react-native-reanimated') ||
60
+ path === 'expo-linear-gradient'
61
+ ) {
62
+ return proxyWorm
63
+ }
64
+ if (
65
+ path.startsWith('react-native') &&
66
+ // allow our rnw.tsx imports through
67
+ !path.startsWith('react-native-web/dist/cjs/exports')
68
+ ) {
69
+ return rnw
70
+ }
71
+ try {
72
+ return og.apply(this, arguments)
73
+ } catch (err: any) {
74
+ console.error('Tamagui error loading file:\n', path, err.message, '\n', err.stack)
75
+ // avoid infinite loops
76
+ process.exit(1)
77
+ }
78
+ }
79
+
80
+ // import config
81
+ const exp = require(configPath)
82
+ const tamaguiConfig = (exp['default'] || exp) as TamaguiInternalConfig
83
+
84
+ if (!tamaguiConfig || !tamaguiConfig.parsed) {
85
+ try {
86
+ const confPath = require.resolve(configPath)
87
+ console.log(`Received:`, tamaguiConfig)
88
+ throw new Error(`Can't find valid config in ${confPath}`)
89
+ } catch (err) {
90
+ throw err
91
+ }
92
+ }
93
+
94
+ // import components
95
+ const components = {}
96
+ for (const moduleName of props.components) {
97
+ const exported = require(moduleName)
98
+ for (const Name in exported) {
99
+ const val = exported[Name]
100
+ const staticConfig = val?.staticConfig
101
+ if (staticConfig) {
102
+ Object.assign(components, { [Name]: { staticConfig } })
103
+ }
104
+ }
105
+ }
106
+
107
+ // undo shims
108
+ process.env.IS_STATIC = undefined
109
+ Mod.prototype.require = og
110
+
111
+ // set up core-node
112
+ createTamagui(tamaguiConfig as any)
113
+
114
+ loadedTamagui = {
115
+ components,
116
+ tamaguiConfig,
117
+ }
118
+
119
+ // save cache
120
+ // try {
121
+ // writeFileSync(
122
+ // cachePath,
123
+ // JSON.stringify({
124
+ // value: loadedTamagui,
125
+ // mtime: confStat.mtime,
126
+ // })
127
+ // )
128
+ // } catch (err: any) {
129
+ // console.log(`Error: tamagui config not stringifiable, caching disabled "${err.message}"`)
130
+ // }
131
+
132
+ return loadedTamagui
133
+ } catch (err) {
134
+ console.log('err', err)
135
+ throw err
136
+ } finally {
137
+ unregister()
138
+ }
139
+ }
@@ -0,0 +1,16 @@
1
+ const prefix = ' '
2
+
3
+ export const logLines = (str: string, singleLine = false) => {
4
+ if (singleLine) {
5
+ return prefix + str.split(' ').join(`\n${prefix}`)
6
+ }
7
+ let lines: string[] = ['']
8
+ const items = str.split(' ')
9
+ for (const item of items) {
10
+ if (item.length + lines[lines.length - 1].length > 85) {
11
+ lines.push('')
12
+ }
13
+ lines[lines.length - 1] += item + ' '
14
+ }
15
+ return lines.map((line, i) => prefix + (i == 0 ? '' : ' ') + line.trim()).join('\n')
16
+ }
@@ -0,0 +1,63 @@
1
+ import generate from '@babel/generator'
2
+ import * as t from '@babel/types'
3
+ import invariant from 'invariant'
4
+
5
+ import { Ternary } from '../types'
6
+
7
+ export function normalizeTernaries(ternaries: Ternary[]) {
8
+ invariant(
9
+ Array.isArray(ternaries),
10
+ 'extractStaticTernaries expects param 1 to be an array of ternaries'
11
+ )
12
+
13
+ if (ternaries.length === 0) {
14
+ return []
15
+ }
16
+
17
+ const ternariesByKey: { [key: string]: Ternary } = {}
18
+
19
+ for (let idx = -1, len = ternaries.length; ++idx < len; ) {
20
+ const { test, consequent, alternate, remove, ...rest } = ternaries[idx]
21
+
22
+ let ternaryTest = test
23
+
24
+ // strip parens
25
+ if (t.isExpressionStatement(test)) {
26
+ ternaryTest = (test as any).expression
27
+ }
28
+
29
+ // convert `!thing` to `thing` with swapped consequent and alternate
30
+ let shouldSwap = false
31
+ if (t.isUnaryExpression(test) && test.operator === '!') {
32
+ ternaryTest = test.argument
33
+ shouldSwap = true
34
+ } else if (t.isBinaryExpression(test)) {
35
+ if (test.operator === '!==' || test.operator === '!=') {
36
+ ternaryTest = t.binaryExpression(test.operator, test.left, test.right)
37
+ shouldSwap = true
38
+ }
39
+ }
40
+
41
+ const key = generate(ternaryTest).code
42
+
43
+ if (!ternariesByKey[key]) {
44
+ ternariesByKey[key] = {
45
+ ...rest,
46
+ alternate: {},
47
+ consequent: {},
48
+ test: ternaryTest,
49
+ remove,
50
+ }
51
+ }
52
+ const altStyle = (shouldSwap ? consequent : alternate) ?? {}
53
+ const consStyle = (shouldSwap ? alternate : consequent) ?? {}
54
+ Object.assign(ternariesByKey[key].alternate!, altStyle)
55
+ Object.assign(ternariesByKey[key].consequent!, consStyle)
56
+ }
57
+
58
+ const ternaryExpression = Object.keys(ternariesByKey).map((key) => {
59
+ return ternariesByKey[key]
60
+ })
61
+
62
+ return ternaryExpression
63
+ }