@hanzogui/static 2.0.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 (207) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/check-dep-versions.cjs +267 -0
  4. package/dist/checkDeps.cjs +200 -0
  5. package/dist/constants.cjs +49 -0
  6. package/dist/exports.cjs +44 -0
  7. package/dist/extractor/accessSafe.cjs +46 -0
  8. package/dist/extractor/babelParse.cjs +54 -0
  9. package/dist/extractor/buildClassName.cjs +62 -0
  10. package/dist/extractor/bundle.cjs +185 -0
  11. package/dist/extractor/bundleConfig.cjs +513 -0
  12. package/dist/extractor/concatClassName.cjs +80 -0
  13. package/dist/extractor/createEvaluator.cjs +75 -0
  14. package/dist/extractor/createExtractor.cjs +1150 -0
  15. package/dist/extractor/createLogger.cjs +45 -0
  16. package/dist/extractor/detectModuleFormat.cjs +49 -0
  17. package/dist/extractor/ensureImportingConcat.cjs +48 -0
  18. package/dist/extractor/errors.cjs +26 -0
  19. package/dist/extractor/esbuildAliasPlugin.cjs +44 -0
  20. package/dist/extractor/esbuildTsconfigPaths.cjs +86 -0
  21. package/dist/extractor/evaluateAstNode.cjs +83 -0
  22. package/dist/extractor/extractHelpers.cjs +119 -0
  23. package/dist/extractor/extractMediaStyle.cjs +128 -0
  24. package/dist/extractor/extractToClassNames.cjs +321 -0
  25. package/dist/extractor/extractToNative.cjs +266 -0
  26. package/dist/extractor/findTopmostFunction.cjs +34 -0
  27. package/dist/extractor/generatedUid.cjs +47 -0
  28. package/dist/extractor/getGuiConfigPathFromOptionsConfig.cjs +35 -0
  29. package/dist/extractor/getPrefixLogs.cjs +29 -0
  30. package/dist/extractor/getPropValueFromAttributes.cjs +67 -0
  31. package/dist/extractor/getSourceModule.cjs +64 -0
  32. package/dist/extractor/getStaticBindingsForScope.cjs +150 -0
  33. package/dist/extractor/hoistClassNames.cjs +62 -0
  34. package/dist/extractor/literalToAst.cjs +84 -0
  35. package/dist/extractor/loadFile.cjs +11 -0
  36. package/dist/extractor/loadGui.cjs +322 -0
  37. package/dist/extractor/logLines.cjs +35 -0
  38. package/dist/extractor/normalizeTernaries.cjs +73 -0
  39. package/dist/extractor/propsToFontFamilyCache.cjs +38 -0
  40. package/dist/extractor/regenerateConfig.cjs +150 -0
  41. package/dist/extractor/removeUnusedHooks.cjs +76 -0
  42. package/dist/extractor/timer.cjs +43 -0
  43. package/dist/extractor/validHTMLAttributes.cjs +77 -0
  44. package/dist/extractor/watchGuiConfig.cjs +58 -0
  45. package/dist/getPragmaOptions.cjs +58 -0
  46. package/dist/helpers/memoize.cjs +37 -0
  47. package/dist/helpers/requireGuiCore.cjs +33 -0
  48. package/dist/index.cjs +40 -0
  49. package/dist/registerRequire.cjs +115 -0
  50. package/dist/server.cjs +75 -0
  51. package/dist/setup.cjs +0 -0
  52. package/dist/types.cjs +16 -0
  53. package/dist/worker.cjs +89 -0
  54. package/package.json +101 -0
  55. package/src/check-dep-versions.ts +738 -0
  56. package/src/checkDeps.ts +371 -0
  57. package/src/constants.ts +14 -0
  58. package/src/exports.ts +15 -0
  59. package/src/extractor/accessSafe.ts +22 -0
  60. package/src/extractor/babelParse.ts +37 -0
  61. package/src/extractor/buildClassName.ts +76 -0
  62. package/src/extractor/bundle.ts +277 -0
  63. package/src/extractor/bundleConfig.ts +958 -0
  64. package/src/extractor/concatClassName.ts +105 -0
  65. package/src/extractor/createEvaluator.ts +82 -0
  66. package/src/extractor/createExtractor.ts +2665 -0
  67. package/src/extractor/createLogger.ts +41 -0
  68. package/src/extractor/detectModuleFormat.ts +42 -0
  69. package/src/extractor/ensureImportingConcat.ts +36 -0
  70. package/src/extractor/errors.ts +1 -0
  71. package/src/extractor/esbuildAliasPlugin.ts +40 -0
  72. package/src/extractor/esbuildTsconfigPaths.ts +103 -0
  73. package/src/extractor/evaluateAstNode.ts +136 -0
  74. package/src/extractor/extractHelpers.ts +211 -0
  75. package/src/extractor/extractMediaStyle.ts +194 -0
  76. package/src/extractor/extractToClassNames.ts +631 -0
  77. package/src/extractor/extractToNative.ts +463 -0
  78. package/src/extractor/findTopmostFunction.ts +24 -0
  79. package/src/extractor/generatedUid.ts +41 -0
  80. package/src/extractor/getGuiConfigPathFromOptionsConfig.ts +24 -0
  81. package/src/extractor/getPrefixLogs.ts +9 -0
  82. package/src/extractor/getPropValueFromAttributes.ts +95 -0
  83. package/src/extractor/getSourceModule.ts +96 -0
  84. package/src/extractor/getStaticBindingsForScope.ts +237 -0
  85. package/src/extractor/hoistClassNames.ts +52 -0
  86. package/src/extractor/literalToAst.ts +85 -0
  87. package/src/extractor/loadFile.ts +17 -0
  88. package/src/extractor/loadGui.ts +487 -0
  89. package/src/extractor/logLines.ts +16 -0
  90. package/src/extractor/normalizeTernaries.ts +75 -0
  91. package/src/extractor/propsToFontFamilyCache.ts +18 -0
  92. package/src/extractor/regenerateConfig.ts +186 -0
  93. package/src/extractor/removeUnusedHooks.ts +83 -0
  94. package/src/extractor/timer.ts +25 -0
  95. package/src/extractor/validHTMLAttributes.ts +52 -0
  96. package/src/extractor/watchGuiConfig.ts +57 -0
  97. package/src/getPragmaOptions.ts +57 -0
  98. package/src/helpers/memoize.ts +24 -0
  99. package/src/helpers/requireGuiCore.ts +28 -0
  100. package/src/index.ts +4 -0
  101. package/src/registerRequire.ts +236 -0
  102. package/src/server.ts +43 -0
  103. package/src/setup.ts +0 -0
  104. package/src/types.ts +109 -0
  105. package/src/worker.ts +142 -0
  106. package/types/check-dep-versions.d.ts +37 -0
  107. package/types/check-dep-versions.d.ts.map +1 -0
  108. package/types/checkDeps.d.ts +19 -0
  109. package/types/checkDeps.d.ts.map +1 -0
  110. package/types/constants.d.ts +6 -0
  111. package/types/constants.d.ts.map +1 -0
  112. package/types/exports.d.ts +16 -0
  113. package/types/exports.d.ts.map +1 -0
  114. package/types/extractor/accessSafe.d.ts +3 -0
  115. package/types/extractor/accessSafe.d.ts.map +1 -0
  116. package/types/extractor/babelParse.d.ts +5 -0
  117. package/types/extractor/babelParse.d.ts.map +1 -0
  118. package/types/extractor/buildClassName.d.ts +7 -0
  119. package/types/extractor/buildClassName.d.ts.map +1 -0
  120. package/types/extractor/bundle.d.ts +121 -0
  121. package/types/extractor/bundle.d.ts.map +1 -0
  122. package/types/extractor/bundleConfig.d.ts +49 -0
  123. package/types/extractor/bundleConfig.d.ts.map +1 -0
  124. package/types/extractor/concatClassName.d.ts +8 -0
  125. package/types/extractor/concatClassName.d.ts.map +1 -0
  126. package/types/extractor/createEvaluator.d.ts +12 -0
  127. package/types/extractor/createEvaluator.d.ts.map +1 -0
  128. package/types/extractor/createExtractor.d.ts +32 -0
  129. package/types/extractor/createExtractor.d.ts.map +1 -0
  130. package/types/extractor/createLogger.d.ts +3 -0
  131. package/types/extractor/createLogger.d.ts.map +1 -0
  132. package/types/extractor/detectModuleFormat.d.ts +5 -0
  133. package/types/extractor/detectModuleFormat.d.ts.map +1 -0
  134. package/types/extractor/ensureImportingConcat.d.ts +4 -0
  135. package/types/extractor/ensureImportingConcat.d.ts.map +1 -0
  136. package/types/extractor/errors.d.ts +3 -0
  137. package/types/extractor/errors.d.ts.map +1 -0
  138. package/types/extractor/esbuildAliasPlugin.d.ts +18 -0
  139. package/types/extractor/esbuildAliasPlugin.d.ts.map +1 -0
  140. package/types/extractor/esbuildTsconfigPaths.d.ts +11 -0
  141. package/types/extractor/esbuildTsconfigPaths.d.ts.map +1 -0
  142. package/types/extractor/evaluateAstNode.d.ts +3 -0
  143. package/types/extractor/evaluateAstNode.d.ts.map +1 -0
  144. package/types/extractor/extractHelpers.d.ts +28 -0
  145. package/types/extractor/extractHelpers.d.ts.map +1 -0
  146. package/types/extractor/extractMediaStyle.d.ts +11 -0
  147. package/types/extractor/extractMediaStyle.d.ts.map +1 -0
  148. package/types/extractor/extractToClassNames.d.ts +25 -0
  149. package/types/extractor/extractToClassNames.d.ts.map +1 -0
  150. package/types/extractor/extractToNative.d.ts +13 -0
  151. package/types/extractor/extractToNative.d.ts.map +1 -0
  152. package/types/extractor/findTopmostFunction.d.ts +4 -0
  153. package/types/extractor/findTopmostFunction.d.ts.map +1 -0
  154. package/types/extractor/generatedUid.d.ts +5 -0
  155. package/types/extractor/generatedUid.d.ts.map +1 -0
  156. package/types/extractor/getGuiConfigPathFromOptionsConfig.d.ts +3 -0
  157. package/types/extractor/getGuiConfigPathFromOptionsConfig.d.ts.map +1 -0
  158. package/types/extractor/getPrefixLogs.d.ts +3 -0
  159. package/types/extractor/getPrefixLogs.d.ts.map +1 -0
  160. package/types/extractor/getPropValueFromAttributes.d.ts +19 -0
  161. package/types/extractor/getPropValueFromAttributes.d.ts.map +1 -0
  162. package/types/extractor/getSourceModule.d.ts +16 -0
  163. package/types/extractor/getSourceModule.d.ts.map +1 -0
  164. package/types/extractor/getStaticBindingsForScope.d.ts +5 -0
  165. package/types/extractor/getStaticBindingsForScope.d.ts.map +1 -0
  166. package/types/extractor/hoistClassNames.d.ts +6 -0
  167. package/types/extractor/hoistClassNames.d.ts.map +1 -0
  168. package/types/extractor/literalToAst.d.ts +4 -0
  169. package/types/extractor/literalToAst.d.ts.map +1 -0
  170. package/types/extractor/loadFile.d.ts +1 -0
  171. package/types/extractor/loadFile.d.ts.map +1 -0
  172. package/types/extractor/loadGui.d.ts +22 -0
  173. package/types/extractor/loadGui.d.ts.map +1 -0
  174. package/types/extractor/logLines.d.ts +2 -0
  175. package/types/extractor/logLines.d.ts.map +1 -0
  176. package/types/extractor/normalizeTernaries.d.ts +3 -0
  177. package/types/extractor/normalizeTernaries.d.ts.map +1 -0
  178. package/types/extractor/propsToFontFamilyCache.d.ts +4 -0
  179. package/types/extractor/propsToFontFamilyCache.d.ts.map +1 -0
  180. package/types/extractor/regenerateConfig.d.ts +9 -0
  181. package/types/extractor/regenerateConfig.d.ts.map +1 -0
  182. package/types/extractor/removeUnusedHooks.d.ts +3 -0
  183. package/types/extractor/removeUnusedHooks.d.ts.map +1 -0
  184. package/types/extractor/timer.d.ts +5 -0
  185. package/types/extractor/timer.d.ts.map +1 -0
  186. package/types/extractor/validHTMLAttributes.d.ts +52 -0
  187. package/types/extractor/validHTMLAttributes.d.ts.map +1 -0
  188. package/types/extractor/watchGuiConfig.d.ts +5 -0
  189. package/types/extractor/watchGuiConfig.d.ts.map +1 -0
  190. package/types/getPragmaOptions.d.ts +8 -0
  191. package/types/getPragmaOptions.d.ts.map +1 -0
  192. package/types/helpers/memoize.d.ts +8 -0
  193. package/types/helpers/memoize.d.ts.map +1 -0
  194. package/types/helpers/requireGuiCore.d.ts +3 -0
  195. package/types/helpers/requireGuiCore.d.ts.map +1 -0
  196. package/types/index.d.ts +4 -0
  197. package/types/index.d.ts.map +1 -0
  198. package/types/registerRequire.d.ts +10 -0
  199. package/types/registerRequire.d.ts.map +1 -0
  200. package/types/server.d.ts +3 -0
  201. package/types/server.d.ts.map +1 -0
  202. package/types/setup.d.ts +1 -0
  203. package/types/setup.d.ts.map +1 -0
  204. package/types/types.d.ts +92 -0
  205. package/types/types.d.ts.map +1 -0
  206. package/types/worker.d.ts +46 -0
  207. package/types/worker.d.ts.map +1 -0
@@ -0,0 +1,2665 @@
1
+ import type { NodePath, TraverseOptions } from '@babel/traverse'
2
+ import traverse from '@babel/traverse'
3
+ import * as t from '@babel/types'
4
+ import { Color, colorLog } from '@hanzogui/cli-color'
5
+ import * as reactNativeWebInternals from '@hanzogui/react-native-web-internals'
6
+ import {
7
+ StyleObjectIdentifier,
8
+ StyleObjectRules,
9
+ type GetStyleState,
10
+ type PseudoStyles,
11
+ type SplitStyleProps,
12
+ type StaticConfig,
13
+ type GuiComponentState,
14
+ } from '@hanzogui/web'
15
+ import { existsSync, readFileSync } from 'node:fs'
16
+ import { basename, dirname, resolve, relative } from 'node:path'
17
+ import { nodeModuleNameResolver, sys } from 'typescript'
18
+ import type { ViewStyle } from 'react-native'
19
+
20
+ import { FAILED_EVAL } from '../constants'
21
+ import { requireGuiCore } from '../helpers/requireGuiCore'
22
+ import type {
23
+ ExtractedAttr,
24
+ ExtractedAttrStyle,
25
+ ExtractorOptions,
26
+ ExtractorParseProps,
27
+ GuiOptions,
28
+ GuiOptionsWithFileInfo,
29
+ Ternary,
30
+ } from '../types'
31
+ import type { LoadedComponents, GuiProjectInfo } from './bundleConfig'
32
+ import { createEvaluator, createSafeEvaluator } from './createEvaluator'
33
+ import { evaluateAstNode } from './evaluateAstNode'
34
+ import {
35
+ attrStr,
36
+ findComponentName,
37
+ getValidComponent,
38
+ getValidComponentsPaths,
39
+ getValidImport,
40
+ isPresent,
41
+ isValidImport,
42
+ objToStr,
43
+ } from './extractHelpers'
44
+ import { findTopmostFunction } from './findTopmostFunction'
45
+ import { cleanupBeforeExit, getStaticBindingsForScope } from './getStaticBindingsForScope'
46
+ import { literalToAst } from './literalToAst'
47
+ import { loadGui, loadGuiSync } from './loadGui'
48
+ import { logLines } from './logLines'
49
+ import { normalizeTernaries } from './normalizeTernaries'
50
+ import { setPropsToFontFamily } from './propsToFontFamilyCache'
51
+ import { timer } from './timer'
52
+ import { validHTMLAttributes } from './validHTMLAttributes'
53
+ import { BailOptimizationError } from './errors'
54
+ import { loadCompilerOptionsFromTsconfig } from './esbuildTsconfigPaths'
55
+
56
+ const UNTOUCHED_PROPS = {
57
+ key: true,
58
+ style: true,
59
+ className: true,
60
+ }
61
+
62
+ const createTernary = (x: Ternary) => x
63
+
64
+ export type Extractor = ReturnType<typeof createExtractor>
65
+
66
+ type FileOrPath = NodePath<t.Program> | t.File
67
+
68
+ let hasLoggedBaseInfo = false
69
+
70
+ function isFullyDisabled(props: GuiOptions) {
71
+ return props.disableExtraction && props.disableDebugAttr
72
+ }
73
+
74
+ export function createExtractor(
75
+ { logger = console, platform = 'web' }: ExtractorOptions = { logger: console }
76
+ ) {
77
+ if (!process.env.HANZO_GUI_TARGET) {
78
+ throw new Error('Please set process.env.HANZO_GUI_TARGET to either "web" or "native"')
79
+ }
80
+
81
+ const INLINE_EXTRACTABLE = {
82
+ ref: 'ref',
83
+ key: 'key',
84
+ ...(platform === 'web' && {
85
+ onPress: 'onClick',
86
+ onHoverIn: 'onMouseEnter',
87
+ onHoverOut: 'onMouseLeave',
88
+ onPressIn: 'onMouseDown',
89
+ onPressOut: 'onMouseUp',
90
+ }),
91
+ ...(platform === 'native' && {
92
+ // native view props that should pass through without preventing flattening
93
+ testID: 'testID',
94
+ nativeID: 'nativeID',
95
+ accessibilityLabel: 'accessibilityLabel',
96
+ accessibilityHint: 'accessibilityHint',
97
+ accessibilityRole: 'accessibilityRole',
98
+ accessibilityState: 'accessibilityState',
99
+ accessibilityValue: 'accessibilityValue',
100
+ accessibilityActions: 'accessibilityActions',
101
+ accessibilityLabelledBy: 'accessibilityLabelledBy',
102
+ accessibilityLiveRegion: 'accessibilityLiveRegion',
103
+ accessibilityElementsHidden: 'accessibilityElementsHidden',
104
+ accessibilityViewIsModal: 'accessibilityViewIsModal',
105
+ importantForAccessibility: 'importantForAccessibility',
106
+ collapsable: 'collapsable',
107
+ needsOffscreenAlphaCompositing: 'needsOffscreenAlphaCompositing',
108
+ removeClippedSubviews: 'removeClippedSubviews',
109
+ renderToHardwareTextureAndroid: 'renderToHardwareTextureAndroid',
110
+ shouldRasterizeIOS: 'shouldRasterizeIOS',
111
+ hitSlop: 'hitSlop',
112
+ pointerEvents: 'pointerEvents',
113
+ }),
114
+ }
115
+
116
+ const componentState: GuiComponentState = {
117
+ focus: false,
118
+ focusVisible: false,
119
+ focusWithin: false,
120
+ hover: false,
121
+ unmounted: true,
122
+ press: false,
123
+ pressIn: false,
124
+ disabled: false,
125
+ } as const
126
+
127
+ const styleProps: SplitStyleProps = {
128
+ resolveValues: process.env.HANZO_GUI_TARGET === 'native' ? 'value' : 'variable',
129
+ noClass: false,
130
+ isAnimated: false,
131
+ }
132
+
133
+ const shouldAddDebugProp =
134
+ // really basic disable this for next.js because it messes with ssr
135
+ !process.env.npm_package_dependencies_next &&
136
+ process.env.HANZO_GUI_TARGET !== 'native' &&
137
+ process.env.IDENTIFY_TAGS !== 'false' &&
138
+ (process.env.NODE_ENV === 'development' || process.env.IDENTIFY_TAGS)
139
+
140
+ let projectInfo: GuiProjectInfo | null = null
141
+
142
+ // cache of dynamically discovered styled components, keyed by absolute file path
143
+ // persists across files within the same worker/extractor instance
144
+ const dynamicComponentCache = new Map<string, LoadedComponents>()
145
+ const dynamicLoadingInProgress = new Set<string>()
146
+
147
+ // lazily loaded tsconfig compiler options for path alias resolution
148
+ let _compilerOptions: any = null
149
+ function getCompilerOptions() {
150
+ if (!_compilerOptions) {
151
+ try {
152
+ _compilerOptions = loadCompilerOptionsFromTsconfig()
153
+ } catch {
154
+ _compilerOptions = {}
155
+ }
156
+ }
157
+ return _compilerOptions
158
+ }
159
+
160
+ function resolveImportPath(fromFile: string, importPath: string): string | null {
161
+ if (importPath.startsWith('.')) {
162
+ // relative path resolution
163
+ const dir = dirname(fromFile)
164
+ const base = resolve(dir, importPath)
165
+ const extensions = ['.tsx', '.ts', '.jsx', '.js']
166
+ for (const ext of extensions) {
167
+ const full = base + ext
168
+ if (existsSync(full)) return full
169
+ }
170
+ // try index files
171
+ for (const ext of extensions) {
172
+ const full = resolve(base, `index${ext}`)
173
+ if (existsSync(full)) return full
174
+ }
175
+ return null
176
+ }
177
+
178
+ // tsconfig path alias resolution (e.g. ~/foo, @/bar)
179
+ const compilerOptions = getCompilerOptions()
180
+ if (compilerOptions.paths) {
181
+ try {
182
+ const { resolvedModule } = nodeModuleNameResolver(
183
+ importPath,
184
+ fromFile,
185
+ compilerOptions,
186
+ sys
187
+ )
188
+ if (
189
+ resolvedModule &&
190
+ !resolvedModule.resolvedFileName.endsWith('.d.ts') &&
191
+ !resolvedModule.isExternalLibraryImport
192
+ ) {
193
+ return resolvedModule.resolvedFileName
194
+ }
195
+ } catch {
196
+ // fallback - tsconfig resolution failed
197
+ }
198
+ }
199
+
200
+ return null
201
+ }
202
+
203
+ const styledCheckCache = new Map<string, boolean>()
204
+
205
+ function mightHaveStyledComponents(filePath: string): boolean {
206
+ const cached = styledCheckCache.get(filePath)
207
+ if (cached !== undefined) return cached
208
+ try {
209
+ const content = readFileSync(filePath, 'utf-8')
210
+ const result = content.includes('styled(')
211
+ styledCheckCache.set(filePath, result)
212
+ return result
213
+ } catch {
214
+ styledCheckCache.set(filePath, false)
215
+ return false
216
+ }
217
+ }
218
+
219
+ // we load hanzo-gui delayed because we need to set some global/env stuff before importing
220
+ // otherwise we'd import `rnw` and cause it to evaluate react-native-web which causes errors
221
+
222
+ function loadSync(props: GuiOptions) {
223
+ if (isFullyDisabled(props)) {
224
+ return null
225
+ }
226
+ return (projectInfo ||= loadGuiSync(props))
227
+ }
228
+
229
+ async function load(props: GuiOptions) {
230
+ if (isFullyDisabled(props)) {
231
+ return null
232
+ }
233
+ return (projectInfo ||= await loadGui(props))
234
+ }
235
+
236
+ return {
237
+ options: {
238
+ logger,
239
+ },
240
+ cleanupBeforeExit,
241
+ loadGui: load,
242
+ loadGuiSync: loadSync,
243
+ getGui() {
244
+ return projectInfo?.guiConfig
245
+ },
246
+ parseSync: (f: FileOrPath, props: ExtractorParseProps) => {
247
+ globalThis.expo ||= {} // expo-modules-core checks this and avoids loading "native" modules if exists
248
+ const projectInfo = loadSync(props)
249
+ return parseWithConfig(projectInfo || {}, f, props)
250
+ },
251
+ parse: async (f: FileOrPath, props: ExtractorParseProps) => {
252
+ globalThis.expo ||= {} // expo-modules-core checks this and avoids loading "native" modules if exists
253
+ const projectInfo = await load(props)
254
+ return parseWithConfig(projectInfo || {}, f, props)
255
+ },
256
+ }
257
+
258
+ function parseWithConfig(
259
+ { components, guiConfig }: GuiProjectInfo,
260
+ fileOrPath: FileOrPath,
261
+ options: ExtractorParseProps
262
+ ) {
263
+ const {
264
+ config = 'gui.config.ts',
265
+ importsWhitelist = ['constants.js'],
266
+ evaluateVars = true,
267
+ sourcePath = '',
268
+ onExtractTag,
269
+ onStyledDefinitionRule,
270
+ getFlattenedNode,
271
+ disable,
272
+ disableExtraction,
273
+ disableExtractVariables,
274
+ disableDebugAttr,
275
+ enableDynamicEvaluation = false,
276
+ includeExtensions = ['.ts', '.tsx', '.jsx'],
277
+ extractStyledDefinitions = false,
278
+ prefixLogs,
279
+ excludeProps,
280
+ platform,
281
+ ...restProps
282
+ } = options
283
+
284
+ // invalidate dynamic cache for this file on re-parse (HMR)
285
+ if (sourcePath && dynamicComponentCache.has(sourcePath)) {
286
+ dynamicComponentCache.delete(sourcePath)
287
+ styledCheckCache.delete(sourcePath)
288
+ }
289
+
290
+ if (sourcePath.includes('.gui-dynamic-eval')) {
291
+ return null
292
+ }
293
+
294
+ const {
295
+ normalizeStyle,
296
+ getSplitStyles,
297
+ mediaQueryConfig,
298
+ propMapper,
299
+ proxyThemeVariables,
300
+ getDefaultProps,
301
+ pseudoDescriptors,
302
+ } = requireGuiCore(platform)
303
+
304
+ let shouldPrintDebug = options.shouldPrintDebug || false
305
+
306
+ if (disable === true || (Array.isArray(disable) && disable.includes(sourcePath))) {
307
+ return null
308
+ }
309
+
310
+ if (!isFullyDisabled(options)) {
311
+ if (!components) {
312
+ throw new Error(`Must provide components`)
313
+ }
314
+ }
315
+
316
+ if (
317
+ sourcePath &&
318
+ includeExtensions &&
319
+ !includeExtensions.some((ext) => sourcePath.endsWith(ext))
320
+ ) {
321
+ if (shouldPrintDebug) {
322
+ logger.info(
323
+ `Ignoring file due to includeExtensions: ${sourcePath}, includeExtensions: ${includeExtensions.join(
324
+ ', '
325
+ )}`
326
+ )
327
+ }
328
+ return null
329
+ }
330
+
331
+ function isValidStyleKey(name: string, staticConfig: StaticConfig) {
332
+ if (!projectInfo) {
333
+ throw new Error(`Gui extractor not loaded yet`)
334
+ }
335
+ if (platform === 'native' && name[0] === '$' && mediaQueryConfig[name.slice(1)]) {
336
+ return false
337
+ }
338
+ // Check for $theme-, $platform-, $group- prefixed keys
339
+ if (name[0] === '$') {
340
+ const mediaName = name.slice(1)
341
+ if (
342
+ mediaName.startsWith('theme-') ||
343
+ mediaName.startsWith('platform-') ||
344
+ mediaName.startsWith('group-')
345
+ ) {
346
+ return true
347
+ }
348
+ if (mediaQueryConfig[mediaName]) {
349
+ return true
350
+ }
351
+ }
352
+ return !!(
353
+ staticConfig.validStyles?.[name] ||
354
+ pseudoDescriptors[name] ||
355
+ // don't disable variants or else you lose many things flattening
356
+ staticConfig.variants?.[name] ||
357
+ projectInfo?.guiConfig?.shorthands[name]
358
+ )
359
+ }
360
+
361
+ /**
362
+ * Step 1: Determine if importing any statically extractable components
363
+ */
364
+
365
+ const isTargetingHTML = platform === 'web'
366
+ const ogDebug = shouldPrintDebug
367
+ const tm = timer()
368
+ const propsWithFileInfo: GuiOptionsWithFileInfo = {
369
+ ...options,
370
+ sourcePath,
371
+ allLoadedComponents: components ? [...components] : [],
372
+ }
373
+
374
+ if (!hasLoggedBaseInfo) {
375
+ hasLoggedBaseInfo = true
376
+ if (shouldPrintDebug) {
377
+ logger.info(
378
+ [
379
+ 'loaded components:',
380
+ propsWithFileInfo.allLoadedComponents
381
+ .map((comp) => Object.keys(comp.nameToInfo).join(', '))
382
+ .join(', '),
383
+ ].join(' ')
384
+ )
385
+ }
386
+ if (process.env.DEBUG?.startsWith('@hanzo/gui')) {
387
+ logger.info(
388
+ [
389
+ 'loaded:',
390
+ propsWithFileInfo.allLoadedComponents.map((x) => x.moduleName),
391
+ ].join('\n')
392
+ )
393
+ }
394
+ }
395
+
396
+ tm.mark('load-gui', !!shouldPrintDebug)
397
+
398
+ if (!isFullyDisabled(options)) {
399
+ if (!guiConfig?.themes) {
400
+ console.error(
401
+ `⛔️ Error: Missing "themes" in your gui.config file:
402
+
403
+ You may not need the compiler! Remember you can run Hanzo GUI with no configuration at all.
404
+
405
+ You may have not "export default" your config (you can also "export const config").
406
+
407
+ Or this may be due to duplicated dependency versions:
408
+ - try out https://github.com/bmish/check-dependency-version-consistency to see if there are mis-matches.
409
+ - or search your lockfile for mis-matches.
410
+ `
411
+ )
412
+ console.info(` Got config:`, guiConfig)
413
+ process.exit(0)
414
+ }
415
+ }
416
+
417
+ const firstThemeName = Object.keys(guiConfig?.themes || {})[0]
418
+ const firstTheme = guiConfig?.themes[firstThemeName] || {}
419
+
420
+ if (!firstTheme || typeof firstTheme !== 'object') {
421
+ const err = `Missing theme ${firstThemeName}, an error occurred when importing your config`
422
+ console.info(err, `Got config:`, guiConfig)
423
+ console.info(`Looking for theme:`, firstThemeName)
424
+ throw new Error(err)
425
+ }
426
+
427
+ const proxiedTheme = proxyThemeVariables(firstTheme)
428
+ type AccessListener = (key: string) => void
429
+ const themeAccessListeners = new Set<AccessListener>()
430
+ const defaultTheme = new Proxy(proxiedTheme, {
431
+ get(target, key) {
432
+ if (Reflect.has(target, key)) {
433
+ themeAccessListeners.forEach((cb) => cb(String(key)))
434
+ }
435
+ return Reflect.get(target, key)
436
+ },
437
+ })
438
+
439
+ const body: t.Statement[] | NodePath<t.Statement>[] =
440
+ fileOrPath.type === 'Program' ? fileOrPath.get('body') : fileOrPath.program.body
441
+
442
+ if (!isFullyDisabled(options)) {
443
+ if (Object.keys(components || []).length === 0) {
444
+ console.warn(
445
+ `Warning: Hanzo GUI didn't find any valid components (DEBUG=hanzo-gui for more)`
446
+ )
447
+ if (process.env.DEBUG === '@hanzo/gui') {
448
+ console.info(`components`, Object.keys(components || []), components)
449
+ }
450
+ }
451
+ }
452
+
453
+ if (shouldPrintDebug === 'verbose') {
454
+ logger.info(
455
+ `allLoadedComponent modules ${propsWithFileInfo.allLoadedComponents
456
+ .map((k) => k.moduleName)
457
+ .join(', ')}`
458
+ )
459
+ logger.info(
460
+ `valid import paths: ${JSON.stringify(
461
+ getValidComponentsPaths(propsWithFileInfo)
462
+ )}`
463
+ )
464
+ }
465
+
466
+ let doesUseValidImport = false
467
+ let hasImportedTheme = false
468
+
469
+ const importDeclarations: t.ImportDeclaration[] = []
470
+
471
+ for (const bodyPath of body) {
472
+ if (bodyPath.type !== 'ImportDeclaration') continue
473
+ const node = ('node' in bodyPath ? bodyPath.node : bodyPath) as t.ImportDeclaration
474
+ const moduleName = node.source.value
475
+
476
+ // if importing valid module
477
+ const valid = isValidImport(propsWithFileInfo, moduleName)
478
+
479
+ if (valid) {
480
+ importDeclarations.push(node)
481
+ }
482
+
483
+ if (shouldPrintDebug === 'verbose') {
484
+ logger.info(` - import via ${moduleName} ${valid}`)
485
+ }
486
+
487
+ if (extractStyledDefinitions && enableDynamicEvaluation) {
488
+ // check all imports for `styled`, not just valid packages
489
+ // styled( is basically guaranteed to be hanzo-gui regardless of source
490
+ if (node.specifiers.some((specifier) => specifier.local.name === 'styled')) {
491
+ doesUseValidImport = true
492
+ // don't break - need to collect all import declarations for the styled() handler
493
+ }
494
+ }
495
+
496
+ if (valid) {
497
+ const names = node.specifiers.map((specifier) => specifier.local.name)
498
+ const isValidComponent = names.some((name) =>
499
+ Boolean(isValidImport(propsWithFileInfo, moduleName, name))
500
+ )
501
+ if (shouldPrintDebug === 'verbose') {
502
+ logger.info(
503
+ ` - import ${isValidComponent ? '✅' : '⇣'} - ${names.join(
504
+ ', '
505
+ )} via package '${moduleName}' - (valid: ${JSON.stringify(
506
+ getValidComponentsPaths(propsWithFileInfo)
507
+ )})`
508
+ )
509
+ }
510
+ if (isValidComponent) {
511
+ doesUseValidImport = true
512
+ if (!(extractStyledDefinitions && enableDynamicEvaluation)) break
513
+ }
514
+ }
515
+ }
516
+
517
+ if (shouldPrintDebug) {
518
+ logger.info(
519
+ `${JSON.stringify({ doesUseValidImport, hasImportedTheme }, null, 2)}\n`
520
+ )
521
+ }
522
+
523
+ if (
524
+ !doesUseValidImport &&
525
+ extractStyledDefinitions &&
526
+ enableDynamicEvaluation &&
527
+ sourcePath
528
+ ) {
529
+ // check if any local import is in the dynamic cache or has styled components
530
+ for (const bodyPath of body) {
531
+ if (bodyPath.type !== 'ImportDeclaration') continue
532
+ const node = (
533
+ 'node' in bodyPath ? bodyPath.node : bodyPath
534
+ ) as t.ImportDeclaration
535
+ const moduleName = node.source.value
536
+
537
+ const resolved = resolveImportPath(sourcePath, moduleName)
538
+ if (!resolved) continue
539
+
540
+ if (dynamicComponentCache.has(resolved)) {
541
+ doesUseValidImport = true
542
+ break
543
+ }
544
+
545
+ if (mightHaveStyledComponents(resolved)) {
546
+ doesUseValidImport = true
547
+ break
548
+ }
549
+ }
550
+ }
551
+
552
+ if (!doesUseValidImport) {
553
+ return null
554
+ }
555
+
556
+ function getValidImportedComponent(componentName: string) {
557
+ const importDeclaration = importDeclarations.find((dec) =>
558
+ dec.specifiers.some((spec) => spec.local.name === componentName)
559
+ )
560
+ if (!importDeclaration) {
561
+ return null
562
+ }
563
+ return getValidImport(
564
+ propsWithFileInfo,
565
+ importDeclaration.source.value,
566
+ componentName
567
+ )
568
+ }
569
+
570
+ tm.mark('import-check', !!shouldPrintDebug)
571
+
572
+ let couldntParse = false
573
+ const modifiedComponents = new Set<NodePath<any>>()
574
+
575
+ // only keeping a cache around per-file, reset it if it changes
576
+ const bindingCache: Record<string, string | null> = {}
577
+
578
+ const callTraverse = (a: TraverseOptions<any>) => {
579
+ // @ts-ignore
580
+ return fileOrPath.type === 'File' ? traverse(fileOrPath, a) : fileOrPath.traverse(a)
581
+ }
582
+
583
+ const shouldDisableExtraction =
584
+ disableExtraction === true ||
585
+ (Array.isArray(disableExtraction) && disableExtraction.includes(sourcePath))
586
+
587
+ /**
588
+ * Step 2: Statically extract from JSX < /> nodes
589
+ */
590
+ let programPath: NodePath<t.Program> | null = null
591
+
592
+ const res = {
593
+ styled: 0,
594
+ flattened: 0,
595
+ optimized: 0,
596
+ modified: 0,
597
+ found: 0,
598
+ }
599
+
600
+ const version = `${Math.random()}`
601
+
602
+ callTraverse({
603
+ // @ts-ignore
604
+ Program: {
605
+ enter(path) {
606
+ programPath = path
607
+ },
608
+ },
609
+
610
+ // styled() calls
611
+ CallExpression(path) {
612
+ if (disable || shouldDisableExtraction || extractStyledDefinitions === false) {
613
+ return
614
+ }
615
+
616
+ if (!t.isIdentifier(path.node.callee) || path.node.callee.name !== 'styled') {
617
+ return
618
+ }
619
+
620
+ const variableName =
621
+ t.isVariableDeclarator(path.parent) && t.isIdentifier(path.parent.id)
622
+ ? path.parent.id.name
623
+ : 'unknown'
624
+
625
+ if (shouldPrintDebug) {
626
+ logger.info(` [styled] Found styled(${variableName})`)
627
+ }
628
+
629
+ const parentNode = path.node.arguments[0]
630
+
631
+ if (!t.isIdentifier(parentNode)) {
632
+ return
633
+ }
634
+ const parentName = parentNode.name
635
+ const definition = path.node.arguments[1]
636
+
637
+ if (!parentName || !definition || !t.isObjectExpression(definition)) {
638
+ return
639
+ }
640
+
641
+ // look up by parent first (e.g. View in `styled(View, {...})`), then by self
642
+ let Component =
643
+ getValidImportedComponent(parentName) || getValidImportedComponent(variableName)
644
+
645
+ if (!Component) {
646
+ if (!enableDynamicEvaluation) {
647
+ return
648
+ }
649
+
650
+ try {
651
+ if (shouldPrintDebug) {
652
+ logger.info(
653
+ `Unknown component: ${variableName} = styled(${parentName}) attempting dynamic load: ${sourcePath}`
654
+ )
655
+ }
656
+
657
+ const out = loadGuiSync({
658
+ forceExports: true,
659
+ components: [sourcePath],
660
+ cacheKey: version,
661
+ })
662
+
663
+ if (!out?.components) {
664
+ if (shouldPrintDebug) {
665
+ logger.info(`Couldn't load, got ${out}`)
666
+ }
667
+ return
668
+ }
669
+
670
+ propsWithFileInfo.allLoadedComponents = [
671
+ ...propsWithFileInfo.allLoadedComponents,
672
+ ...out.components,
673
+ ]
674
+
675
+ Component = out.components.flatMap((x) => x.nameToInfo[variableName] ?? [])[0]
676
+
677
+ if (!out.cached) {
678
+ const foundNames = out.components
679
+ ?.map((x) => Object.keys(x.nameToInfo).join(', '))
680
+ .join(', ')
681
+ .trim()
682
+
683
+ if (foundNames) {
684
+ colorLog(
685
+ Color.FgYellow,
686
+ ` | Hanzo GUI found dynamic components: ${foundNames}`
687
+ )
688
+ }
689
+ }
690
+ } catch (err: any) {
691
+ if (shouldPrintDebug) {
692
+ logger.info(
693
+ `skip optimize styled(${variableName}), unable to pre-process (DEBUG=hanzo-gui for more)`
694
+ )
695
+ }
696
+ }
697
+ }
698
+
699
+ if (!Component) {
700
+ if (shouldPrintDebug) {
701
+ logger.info(` No component found`)
702
+ }
703
+
704
+ /**
705
+ * We could/should still extract CSS just limited to validStyleProps
706
+ */
707
+ return
708
+ }
709
+
710
+ const componentSkipProps = new Set([
711
+ ...(Component.staticConfig.inlineWhenUnflattened || []),
712
+ ...(Component.staticConfig.inlineProps || []),
713
+ // for now skip variants, will return to them
714
+ 'variants',
715
+ 'defaultVariants',
716
+ // skip fontFamily its basically a "variant", important for theme use to be value always
717
+ 'fontFamily',
718
+ 'name',
719
+ 'focusStyle',
720
+ 'focusVisibleStyle',
721
+ 'focusWithinStyle',
722
+ 'disabledStyle',
723
+ 'hoverStyle',
724
+ 'pressStyle',
725
+ ])
726
+
727
+ // for now dont parse variants, spreads, etc
728
+ const skipped = new Set<t.ObjectProperty | t.SpreadElement | t.ObjectMethod>()
729
+ const styles = {}
730
+ const staticDefaultProps = {}
731
+
732
+ // Generate scope object at this level
733
+ const staticNamespace = getStaticBindingsForScope(
734
+ path.scope,
735
+ importsWhitelist,
736
+ sourcePath,
737
+ bindingCache,
738
+ shouldPrintDebug
739
+ )
740
+
741
+ const attemptEval = !evaluateVars
742
+ ? evaluateAstNode
743
+ : createEvaluator({
744
+ props: propsWithFileInfo,
745
+ staticNamespace,
746
+ sourcePath,
747
+ shouldPrintDebug,
748
+ })
749
+ const attemptEvalSafe = createSafeEvaluator(attemptEval)
750
+
751
+ for (const property of definition.properties) {
752
+ if (
753
+ t.isObjectProperty(property) &&
754
+ (t.isIdentifier(property.key) || t.isStringLiteral(property.key))
755
+ ) {
756
+ const key = t.isIdentifier(property.key)
757
+ ? property.key.name
758
+ : property.key.value
759
+ const defaultPropValue = attemptEvalSafe(property.value)
760
+ if (defaultPropValue !== FAILED_EVAL) {
761
+ staticDefaultProps[key] = defaultPropValue
762
+ }
763
+ }
764
+
765
+ if (
766
+ !t.isObjectProperty(property) ||
767
+ !t.isIdentifier(property.key) ||
768
+ !isValidStyleKey(property.key.name, Component.staticConfig) ||
769
+ // TODO make pseudos and variants work
770
+ // skip pseudos
771
+ pseudoDescriptors[property.key.name] ||
772
+ // skip variants
773
+ Component.staticConfig.variants?.[property.key.name] ||
774
+ componentSkipProps.has(property.key.name)
775
+ ) {
776
+ skipped.add(property)
777
+ continue
778
+ }
779
+ // attempt eval
780
+ const out = attemptEvalSafe(property.value)
781
+ if (out === FAILED_EVAL) {
782
+ skipped.add(property)
783
+ } else {
784
+ styles[property.key.name] = out
785
+ }
786
+ }
787
+
788
+ const out = getSplitStyles(
789
+ styles,
790
+ Component.staticConfig,
791
+ defaultTheme,
792
+ '',
793
+ componentState,
794
+ styleProps,
795
+ undefined,
796
+ undefined,
797
+ undefined,
798
+ undefined,
799
+ false,
800
+ shouldPrintDebug
801
+ )!
802
+
803
+ const classNames = {
804
+ ...out.classNames,
805
+ }
806
+
807
+ // // add in the style object as classnames
808
+ // const atomics = getPropsAtomic(out.style)
809
+ // for (const atomic of atomics) {
810
+ // out.rulesToInsert = out.rulesToInsert || []
811
+ // out.rulesToInsert.push(atomic)
812
+ // classNames[atomic.property] = atomic.identifier
813
+ // }
814
+
815
+ if (shouldPrintDebug) {
816
+ logger.info(
817
+ [
818
+ `Extracted styled(${variableName})\n`,
819
+ JSON.stringify(styles, null, 2),
820
+ '\n classNames:',
821
+ JSON.stringify(classNames, null, 2),
822
+ '\n rulesToInsert:',
823
+ out.rulesToInsert,
824
+ ].join(' ')
825
+ )
826
+ }
827
+
828
+ // don't replace definition values with class name strings -
829
+ // the runtime needs real values for animations, context, and group styles.
830
+ // we only emit the CSS rules so they're available if the runtime uses classNames.
831
+
832
+ if (out.rulesToInsert) {
833
+ for (const key in out.rulesToInsert) {
834
+ const styleObject = out.rulesToInsert[key]
835
+ onStyledDefinitionRule?.(
836
+ styleObject[StyleObjectIdentifier],
837
+ styleObject[StyleObjectRules]
838
+ )
839
+ }
840
+ }
841
+
842
+ res.styled++
843
+
844
+ // register so JSX handler can find this component (same-file and cross-file)
845
+ if (extractStyledDefinitions && enableDynamicEvaluation && Component) {
846
+ const dynamicStaticConfig = {
847
+ ...Component.staticConfig,
848
+ defaultProps: {
849
+ ...Component.staticConfig.defaultProps,
850
+ ...staticDefaultProps,
851
+ },
852
+ }
853
+
854
+ // add to allLoadedComponents with '' so getValidComponent matches when moduleName is ''
855
+ // (same-file styled components have '' as moduleName in JSX handler)
856
+ propsWithFileInfo.allLoadedComponents.push({
857
+ moduleName: '',
858
+ nameToInfo: { [variableName]: { staticConfig: dynamicStaticConfig } },
859
+ })
860
+
861
+ // also cache by file path so other files importing from this path can find it
862
+ if (sourcePath) {
863
+ let existing = dynamicComponentCache.get(sourcePath)
864
+ if (!existing) {
865
+ existing = { moduleName: sourcePath, nameToInfo: {} }
866
+ dynamicComponentCache.set(sourcePath, existing)
867
+ }
868
+ existing.nameToInfo[variableName] = { staticConfig: dynamicStaticConfig }
869
+ }
870
+ }
871
+
872
+ if (shouldPrintDebug) {
873
+ logger.info(`Extracted styled(${variableName})`)
874
+ }
875
+ },
876
+
877
+ JSXElement(traversePath) {
878
+ tm.mark('jsx-element', !!shouldPrintDebug)
879
+
880
+ const node = traversePath.node.openingElement
881
+ const ogAttributes = node.attributes.map((attr) => ({ ...attr }))
882
+ const componentName = findComponentName(traversePath.scope)
883
+ const closingElement = traversePath.node.closingElement
884
+
885
+ // skip non-identifier opening elements (member expressions, etc.)
886
+ if (
887
+ (closingElement && t.isJSXMemberExpression(closingElement?.name)) ||
888
+ !t.isJSXIdentifier(node.name)
889
+ ) {
890
+ if (shouldPrintDebug) {
891
+ logger.info(` skip non-identifier element`)
892
+ }
893
+
894
+ return
895
+ }
896
+
897
+ // validate its a proper import from hanzo-gui (or internally inside gui)
898
+ const binding = traversePath.scope.getBinding(node.name.name)
899
+ let moduleName = ''
900
+ let dynamicComponent: { staticConfig: any } | null = null
901
+
902
+ if (binding) {
903
+ if (t.isImportDeclaration(binding.path.parent)) {
904
+ moduleName = binding.path.parent.source.value
905
+ if (!isValidImport(propsWithFileInfo, moduleName, binding.identifier.name)) {
906
+ // fallback: try dynamic component cache for local imports (relative or tsconfig alias)
907
+ if (enableDynamicEvaluation && sourcePath) {
908
+ const resolved = resolveImportPath(sourcePath, moduleName)
909
+ if (resolved) {
910
+ // check cache first
911
+ const cached = dynamicComponentCache.get(resolved)
912
+ if (cached?.nameToInfo[binding.identifier.name]) {
913
+ dynamicComponent = cached.nameToInfo[binding.identifier.name]
914
+ } else if (
915
+ !dynamicLoadingInProgress.has(resolved) &&
916
+ mightHaveStyledComponents(resolved)
917
+ ) {
918
+ // proactively load the file
919
+ dynamicLoadingInProgress.add(resolved)
920
+ try {
921
+ const out = loadGuiSync({
922
+ forceExports: true,
923
+ components: [resolved],
924
+ })
925
+ if (out?.components) {
926
+ for (const comp of out.components) {
927
+ // merge into cache
928
+ let existing = dynamicComponentCache.get(resolved)
929
+ if (!existing) {
930
+ existing = { moduleName: resolved, nameToInfo: {} }
931
+ dynamicComponentCache.set(resolved, existing)
932
+ }
933
+ Object.assign(existing.nameToInfo, comp.nameToInfo)
934
+ // also add to allLoadedComponents so getValidComponent works
935
+ propsWithFileInfo.allLoadedComponents.push({
936
+ moduleName: resolved,
937
+ nameToInfo: comp.nameToInfo,
938
+ })
939
+ }
940
+ const cachedNow = dynamicComponentCache.get(resolved)
941
+ if (cachedNow?.nameToInfo[binding.identifier.name]) {
942
+ dynamicComponent = cachedNow.nameToInfo[binding.identifier.name]
943
+ }
944
+ }
945
+ } catch (err) {
946
+ if (shouldPrintDebug) {
947
+ logger.info(` - Failed to dynamically load ${resolved}: ${err}`)
948
+ }
949
+ } finally {
950
+ dynamicLoadingInProgress.delete(resolved)
951
+ }
952
+ }
953
+ }
954
+ }
955
+
956
+ if (!dynamicComponent) {
957
+ if (shouldPrintDebug) {
958
+ logger.info(
959
+ ` - Binding in component ${componentName} not valid import: "${binding.identifier.name}" isn't in ${moduleName}\n`
960
+ )
961
+ }
962
+ return
963
+ }
964
+ }
965
+ }
966
+ }
967
+
968
+ const component =
969
+ dynamicComponent ||
970
+ getValidComponent(propsWithFileInfo, moduleName, node.name.name)
971
+ if (!component || !component.staticConfig) {
972
+ if (shouldPrintDebug) {
973
+ logger.info(`\n - No Hanzo GUI conf for: ${node.name.name}\n`)
974
+ }
975
+ return
976
+ }
977
+
978
+ const originalNodeName = node.name.name
979
+
980
+ // found a valid tag
981
+ res.found++
982
+
983
+ const filePath = `./${relative(process.cwd(), sourcePath)}`
984
+ const lineNumbers = node.loc
985
+ ? node.loc.start.line +
986
+ (node.loc.start.line !== node.loc.end.line ? `-${node.loc.end.line}` : '')
987
+ : ''
988
+
989
+ const codePosition = `${filePath}:${lineNumbers}`
990
+
991
+ // debug just one
992
+ const debugPropValue = node.attributes
993
+ .filter(
994
+ (n) =>
995
+ t.isJSXAttribute(n) && t.isJSXIdentifier(n.name) && n.name.name === 'debug'
996
+ )
997
+ // @ts-ignore
998
+ .map((n: t.JSXAttribute) => {
999
+ if (n.value === null) return true
1000
+ if (t.isStringLiteral(n.value)) return n.value.value as 'verbose'
1001
+ return false
1002
+ })[0] as boolean | 'verbose' | undefined
1003
+
1004
+ if (debugPropValue) {
1005
+ shouldPrintDebug = debugPropValue
1006
+ }
1007
+
1008
+ if (shouldPrintDebug) {
1009
+ logger.info(
1010
+ `\x1b[33m\x1b[0m ` + `${componentName} | ${codePosition} -------------------`
1011
+ )
1012
+ // prettier-ignore
1013
+ logger.info(
1014
+ [
1015
+ '\x1b[1m',
1016
+ '\x1b[32m',
1017
+ `<${originalNodeName} />`,
1018
+ disableDebugAttr ? '' : '🐛',
1019
+ ].join(' ')
1020
+ )
1021
+ }
1022
+
1023
+ // add data-* debug attributes
1024
+ if (platform !== 'native') {
1025
+ if (shouldAddDebugProp && !disableDebugAttr) {
1026
+ res.modified++
1027
+ node.attributes.unshift(
1028
+ t.jsxAttribute(t.jsxIdentifier('data-is'), t.stringLiteral(node.name.name))
1029
+ )
1030
+ if (componentName) {
1031
+ node.attributes.unshift(
1032
+ t.jsxAttribute(t.jsxIdentifier('data-in'), t.stringLiteral(componentName))
1033
+ )
1034
+ }
1035
+
1036
+ node.attributes.unshift(
1037
+ t.jsxAttribute(
1038
+ t.jsxIdentifier('data-at'),
1039
+ t.stringLiteral(`${basename(filePath)}:${lineNumbers}`)
1040
+ )
1041
+ )
1042
+ }
1043
+ }
1044
+
1045
+ if (shouldDisableExtraction) {
1046
+ if (shouldPrintDebug === 'verbose') {
1047
+ logger.info(` ❌ Extraction disabled: ${JSON.stringify(disableExtraction)}\n`)
1048
+ }
1049
+ return
1050
+ }
1051
+
1052
+ try {
1053
+ const { staticConfig } = component
1054
+
1055
+ const defaultProps = {
1056
+ ...getDefaultProps(staticConfig),
1057
+ }
1058
+ const variants = staticConfig.variants || {}
1059
+ const isTextView = staticConfig.isText || false
1060
+ const validStyles = staticConfig?.validStyles ?? {}
1061
+
1062
+ // find render="a" render="main" etc dom indicators
1063
+ let tagName = defaultProps.render ?? (isTextView ? 'span' : 'div')
1064
+ traversePath
1065
+ .get('openingElement')
1066
+ .get('attributes')
1067
+ .forEach((path) => {
1068
+ const attr = path.node
1069
+ if (t.isJSXSpreadAttribute(attr)) return
1070
+ if (attr.name.name !== 'render') return
1071
+ const val = attr.value
1072
+ if (!t.isStringLiteral(val)) return
1073
+ tagName = val.value
1074
+ })
1075
+
1076
+ if (shouldPrintDebug === 'verbose') {
1077
+ console.info(` Start tag ${tagName}`)
1078
+ }
1079
+
1080
+ const flatNodeName = getFlattenedNode?.({ isTextView, tag: tagName })
1081
+
1082
+ const inlineProps = new Set([
1083
+ // adding some always inline props
1084
+ ...(restProps.inlineProps || []),
1085
+ ...(staticConfig.inlineProps || []),
1086
+ ])
1087
+
1088
+ const deoptProps = new Set([
1089
+ // always de-opt animation these
1090
+ 'animation',
1091
+ 'animateOnly',
1092
+ 'animatePresence',
1093
+ 'disableOptimization',
1094
+
1095
+ ...(!isTargetingHTML
1096
+ ? [
1097
+ 'pressStyle',
1098
+ 'focusStyle',
1099
+ 'focusVisibleStyle',
1100
+ 'focusWithinStyle',
1101
+ 'disabledStyle',
1102
+ ]
1103
+ : []),
1104
+
1105
+ // when using a non-CSS driver, de-opt on enterStyle/exitStyle
1106
+ ...(guiConfig?.animations.isReactNative
1107
+ ? ['enterStyle', 'exitStyle']
1108
+ : []),
1109
+ ])
1110
+
1111
+ const inlineWhenUnflattened = new Set(staticConfig.inlineWhenUnflattened || [])
1112
+
1113
+ // Generate scope object at this level
1114
+ const staticNamespace = getStaticBindingsForScope(
1115
+ traversePath.scope,
1116
+ importsWhitelist,
1117
+ sourcePath,
1118
+ bindingCache,
1119
+ shouldPrintDebug
1120
+ )
1121
+
1122
+ const attemptEval = !evaluateVars
1123
+ ? evaluateAstNode
1124
+ : createEvaluator({
1125
+ props: propsWithFileInfo,
1126
+ staticNamespace,
1127
+ sourcePath,
1128
+ traversePath,
1129
+ shouldPrintDebug,
1130
+ })
1131
+ const attemptEvalSafe = createSafeEvaluator(attemptEval)
1132
+
1133
+ if (shouldPrintDebug) {
1134
+ logger.info(` staticNamespace ${Object.keys(staticNamespace).join(', ')}`)
1135
+ }
1136
+
1137
+ //
1138
+ // SPREADS SETUP
1139
+ //
1140
+
1141
+ if (couldntParse) {
1142
+ return
1143
+ }
1144
+
1145
+ tm.mark('jsx-element-flattened', !!shouldPrintDebug)
1146
+
1147
+ let attrs: ExtractedAttr[] = []
1148
+ let shouldDeopt = false
1149
+ const inlined = new Map<string, any>()
1150
+ const variantValues = new Map<string, any>()
1151
+ let hasSetOptimized = false
1152
+ const inlineWhenUnflattenedOGVals = {}
1153
+
1154
+ // RUN first pass
1155
+
1156
+ // normalize all conditionals so we can evaluate away easier later
1157
+ // at the same time lets normalize shorthand media queries into spreads:
1158
+ // that way we can parse them with the same logic later on
1159
+ //
1160
+ // {...media.sm && { color: x ? 'red' : 'blue' }}
1161
+ // => {...media.sm && x && { color: 'red' }}
1162
+ // => {...media.sm && !x && { color: 'blue' }}
1163
+ //
1164
+ // $sm={{ color: 'red' }}
1165
+ // => {...media.sm && { color: 'red' }}
1166
+ //
1167
+ // $sm={{ color: x ? 'red' : 'blue' }}
1168
+ // => {...media.sm && x && { color: 'red' }}
1169
+ // => {...media.sm && !x && { color: 'blue' }}
1170
+
1171
+ const propMapperStyleState: GetStyleState = {
1172
+ staticConfig,
1173
+ usedKeys: {},
1174
+ classNames: {},
1175
+ style: {},
1176
+ theme: defaultTheme,
1177
+ viewProps: defaultProps,
1178
+ conf: guiConfig!,
1179
+ props: defaultProps,
1180
+ componentState,
1181
+ styleProps: {
1182
+ ...styleProps,
1183
+ resolveValues: 'auto',
1184
+ },
1185
+ debug: shouldPrintDebug,
1186
+ }
1187
+
1188
+ attrs = traversePath
1189
+ .get('openingElement')
1190
+ .get('attributes')
1191
+ .flatMap((path) => {
1192
+ // avoid work
1193
+ if (shouldDeopt) {
1194
+ return
1195
+ }
1196
+
1197
+ try {
1198
+ const res = evaluateAttribute(path)
1199
+ if (!res) {
1200
+ path.remove()
1201
+ }
1202
+
1203
+ return res
1204
+ } catch (err: any) {
1205
+ if (shouldPrintDebug) {
1206
+ logger.info(
1207
+ [
1208
+ 'Recoverable error extracting attribute',
1209
+ err.message,
1210
+ shouldPrintDebug === 'verbose' ? err.stack : '',
1211
+ ].join(' ')
1212
+ )
1213
+ if (shouldPrintDebug === 'verbose') {
1214
+ logger.info(`node ${path.node?.type}`)
1215
+ }
1216
+ }
1217
+ // dont flatten if we run into error
1218
+ inlined.set(`${Math.random()}`, 'spread')
1219
+ return {
1220
+ type: 'attr',
1221
+ value: path.node,
1222
+ } as const
1223
+ }
1224
+ })
1225
+ .flat(4)
1226
+ .filter(isPresent)
1227
+
1228
+ if (shouldPrintDebug) {
1229
+ logger.info(
1230
+ [' - attrs (before):\n', logLines(attrs.map(attrStr).join(', '))].join(' ')
1231
+ )
1232
+ }
1233
+
1234
+ // START function evaluateAttribute
1235
+ function evaluateAttribute(
1236
+ path: NodePath<t.JSXAttribute | t.JSXSpreadAttribute>
1237
+ ): ExtractedAttr | ExtractedAttr[] | null {
1238
+ const attribute = path.node
1239
+ const attr: ExtractedAttr = { type: 'attr', value: attribute }
1240
+ // ...spreads
1241
+ if (t.isJSXSpreadAttribute(attribute)) {
1242
+ const arg = attribute.argument
1243
+ const conditional = t.isConditionalExpression(arg)
1244
+ ? // <YStack {...isSmall ? { color: 'red } : { color: 'blue }}
1245
+ ([arg.test, arg.consequent, arg.alternate] as const)
1246
+ : t.isLogicalExpression(arg) && arg.operator === '&&'
1247
+ ? // <YStack {...isSmall && { color: 'red }}
1248
+ ([arg.left, arg.right, null] as const)
1249
+ : null
1250
+
1251
+ if (conditional) {
1252
+ const [test, alt, cons] = conditional
1253
+ if (!test) throw new Error(`no test`)
1254
+ if ([alt, cons].some((side) => side && !isStaticObject(side))) {
1255
+ if (shouldPrintDebug) {
1256
+ logger.info(`not extractable ${alt} ${cons}`)
1257
+ }
1258
+ return attr
1259
+ }
1260
+ // split into individual ternaries per object property
1261
+ return [
1262
+ ...(flattenNestedTernaries(test, alt) || []),
1263
+ ...((cons &&
1264
+ flattenNestedTernaries(t.unaryExpression('!', test), cons)) ||
1265
+ []),
1266
+ ].map((ternary) => ({
1267
+ type: 'ternary',
1268
+ value: ternary,
1269
+ }))
1270
+ }
1271
+ }
1272
+ // END ...spreads
1273
+
1274
+ // directly keep these
1275
+ // couldn't evaluate spread, undefined name, or name is not string
1276
+ if (
1277
+ t.isJSXSpreadAttribute(attribute) ||
1278
+ !attribute.name ||
1279
+ typeof attribute.name.name !== 'string'
1280
+ ) {
1281
+ if (shouldPrintDebug) {
1282
+ logger.info(' ! inlining, spread attr')
1283
+ }
1284
+ inlined.set(`${Math.random()}`, 'spread')
1285
+ return attr
1286
+ }
1287
+
1288
+ const name = attribute.name.name
1289
+
1290
+ // in hanzo-gui style is handled at the end of the style loop so its not as simple as just
1291
+ // adding this as a "style" property
1292
+ // its not used often when using hanzo-gui so not optimizing it for now
1293
+ if (name === 'style') {
1294
+ shouldDeopt = true
1295
+ return null
1296
+ }
1297
+
1298
+ if (excludeProps?.has(name)) {
1299
+ if (shouldPrintDebug) {
1300
+ logger.info([' excluding prop', name].join(' '))
1301
+ }
1302
+ return null
1303
+ }
1304
+
1305
+ if (inlineProps.has(name)) {
1306
+ inlined.set(name, name)
1307
+ if (shouldPrintDebug) {
1308
+ logger.info([' ! inlining, inline prop', name].join(' '))
1309
+ }
1310
+ return attr
1311
+ }
1312
+
1313
+ // pass className, key, and style props through untouched
1314
+ if (UNTOUCHED_PROPS[name]) {
1315
+ return attr
1316
+ }
1317
+
1318
+ if (INLINE_EXTRACTABLE[name]) {
1319
+ inlined.set(name, INLINE_EXTRACTABLE[name])
1320
+ return attr
1321
+ }
1322
+
1323
+ if (
1324
+ name.startsWith('data-') ||
1325
+ name.startsWith('aria-') ||
1326
+ validHTMLAttributes[name]
1327
+ ) {
1328
+ return attr
1329
+ }
1330
+
1331
+ // de-opt on enterStyle={expression}
1332
+ if (
1333
+ (name === 'enterStyle' || name === 'exitStyle') &&
1334
+ t.isJSXExpressionContainer(attribute?.value)
1335
+ ) {
1336
+ shouldDeopt = true
1337
+ return attr
1338
+ }
1339
+
1340
+ // shorthand media queries
1341
+ if (name[0] === '$' && t.isJSXExpressionContainer(attribute?.value)) {
1342
+ const shortname = name.slice(1)
1343
+ if (mediaQueryConfig[shortname]) {
1344
+ const expression = attribute.value.expression
1345
+ if (!t.isJSXEmptyExpression(expression)) {
1346
+ const ternaries = flattenNestedTernaries(
1347
+ t.stringLiteral(shortname),
1348
+ expression,
1349
+ {
1350
+ inlineMediaQuery: shortname,
1351
+ }
1352
+ )
1353
+ if (ternaries) {
1354
+ return ternaries.map((value) => ({
1355
+ type: 'ternary',
1356
+ value,
1357
+ }))
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+
1363
+ const [value, valuePath] = (() => {
1364
+ if (t.isJSXExpressionContainer(attribute?.value)) {
1365
+ return [attribute.value.expression!, path.get('value')!] as const
1366
+ }
1367
+ return [attribute.value!, path.get('value')!] as const
1368
+ })()
1369
+
1370
+ const remove = () => {
1371
+ Array.isArray(valuePath)
1372
+ ? valuePath.map((p) => p.remove())
1373
+ : valuePath.remove()
1374
+ }
1375
+
1376
+ if (name === 'ref') {
1377
+ if (shouldPrintDebug) {
1378
+ logger.info([' ! inlining, ref', name].join(' '))
1379
+ }
1380
+ inlined.set('ref', 'ref')
1381
+ return attr
1382
+ }
1383
+
1384
+ if (name === 'render') {
1385
+ // Only optimize string literal render props
1386
+ // JSX elements and functions should deopt
1387
+ if (!value || value.type !== 'StringLiteral') {
1388
+ if (shouldPrintDebug) {
1389
+ logger.info(` ! deopt on render prop (not a string literal)`)
1390
+ }
1391
+ shouldDeopt = true
1392
+ }
1393
+ return {
1394
+ type: 'attr',
1395
+ value: path.node,
1396
+ }
1397
+ }
1398
+
1399
+ // native shouldn't extract variables
1400
+ if (disableExtractVariables === true) {
1401
+ if (value) {
1402
+ if (value.type === 'StringLiteral' && value.value[0] === '$') {
1403
+ if (shouldPrintDebug) {
1404
+ logger.info(
1405
+ [
1406
+ ` ! inlining, native disable extract: ${name} =`,
1407
+ value.value,
1408
+ ].join(' ')
1409
+ )
1410
+ }
1411
+ inlined.set(name, true)
1412
+ return attr
1413
+ }
1414
+ }
1415
+ }
1416
+
1417
+ if (name === 'theme') {
1418
+ inlined.set('theme', attr.value)
1419
+ return attr
1420
+ }
1421
+
1422
+ // if value can be evaluated, extract it and filter it out
1423
+ const styleValue = attemptEvalSafe(value)
1424
+
1425
+ // never flatten if a prop isn't a valid static attribute
1426
+ // only post prop-mapping
1427
+ if (!variants[name] && !isValidStyleKey(name, staticConfig)) {
1428
+ let out: any = null
1429
+
1430
+ // for now passing empty props {}, a bit odd, need to at least document
1431
+ // for now we don't expose custom components so just noting behavior
1432
+ propMapper(name, styleValue, propMapperStyleState, false, (key, val) => {
1433
+ out ||= {}
1434
+ out[key] = val
1435
+ })
1436
+
1437
+ if (out) {
1438
+ if (isTargetingHTML) {
1439
+ // translate to DOM-compat
1440
+ out = reactNativeWebInternals.createDOMProps(
1441
+ isTextView ? 'span' : 'div',
1442
+ out
1443
+ )
1444
+ // remove className - we dont use rnw styling
1445
+ delete out.className
1446
+ }
1447
+ }
1448
+
1449
+ let didInline = false
1450
+ const attributes = Object.keys(out).map((key) => {
1451
+ const val = out[key]
1452
+ const isStyle = isValidStyleKey(key, staticConfig)
1453
+ if (isStyle) {
1454
+ return {
1455
+ type: 'style',
1456
+ value: { [key]: styleValue },
1457
+ name: key,
1458
+ attr: path.node,
1459
+ } as const
1460
+ }
1461
+ if (
1462
+ validHTMLAttributes[key] ||
1463
+ key.startsWith('aria-') ||
1464
+ key.startsWith('data-') ||
1465
+ // this is debug stuff added by vite / new jsx transform
1466
+ key === '__source' ||
1467
+ key === '__self'
1468
+ ) {
1469
+ return attr
1470
+ }
1471
+ if (shouldPrintDebug) {
1472
+ logger.info(' ! inlining, non-static ' + key)
1473
+ }
1474
+ didInline = true
1475
+ inlined.set(key, val)
1476
+ return val
1477
+ })
1478
+
1479
+ // weird logic whats going on here
1480
+ if (didInline) {
1481
+ if (shouldPrintDebug) {
1482
+ logger.info(
1483
+ ` bailing flattening due to attributes ${attributes.map((x) =>
1484
+ x.toString()
1485
+ )}`
1486
+ )
1487
+ }
1488
+ // bail
1489
+ return attr
1490
+ }
1491
+
1492
+ // return evaluated attributes
1493
+ return attributes
1494
+ }
1495
+
1496
+ // FAILED = dynamic or ternary, keep going
1497
+ if (styleValue !== FAILED_EVAL) {
1498
+ if (inlineWhenUnflattened.has(name)) {
1499
+ // preserve original value for restoration
1500
+ inlineWhenUnflattenedOGVals[name] = { styleValue, attr }
1501
+ }
1502
+
1503
+ if (isValidStyleKey(name, staticConfig)) {
1504
+ // $theme-, $group- styles should not be flattened (needs runtime handling)
1505
+ // $platform- can be flattened if the platform matches
1506
+ if (name[0] === '$') {
1507
+ if (name.startsWith('$theme-') || name.startsWith('$group-')) {
1508
+ if (shouldPrintDebug) {
1509
+ logger.info(` ! not flattening media-like style: ${name}`)
1510
+ }
1511
+ inlined.set(name, true)
1512
+ return attr
1513
+ }
1514
+
1515
+ // $platform-web, $platform-native, $platform-ios, $platform-android
1516
+ if (name.startsWith('$platform-')) {
1517
+ const platformName = name.slice(10) // remove '$platform-'
1518
+ const isMatchingPlatform =
1519
+ platformName === platform ||
1520
+ (platformName === 'native' && platform === 'native') ||
1521
+ (platformName === 'web' && platform === 'web')
1522
+
1523
+ if (isMatchingPlatform && typeof styleValue === 'object') {
1524
+ // Flatten the inner styles directly
1525
+ if (shouldPrintDebug) {
1526
+ logger.info(
1527
+ ` flattening $platform-${platformName}: ${JSON.stringify(styleValue)}`
1528
+ )
1529
+ }
1530
+ return Object.entries(styleValue).map(([key, val]) => ({
1531
+ type: 'style' as const,
1532
+ value: { [key]: val },
1533
+ name: key,
1534
+ attr: path.node,
1535
+ }))
1536
+ } else {
1537
+ // Platform doesn't match, skip these styles entirely
1538
+ if (shouldPrintDebug) {
1539
+ logger.info(` ! skipping non-matching platform style: ${name}`)
1540
+ }
1541
+ return []
1542
+ }
1543
+ }
1544
+ }
1545
+ if (shouldPrintDebug) {
1546
+ logger.info(` style: ${name} = ${JSON.stringify(styleValue)}`)
1547
+ }
1548
+ if (!(name in defaultProps)) {
1549
+ if (!hasSetOptimized) {
1550
+ res.optimized++
1551
+ hasSetOptimized = true
1552
+ }
1553
+ }
1554
+ return {
1555
+ type: 'style',
1556
+ value: { [name]: styleValue },
1557
+ name,
1558
+ attr: path.node,
1559
+ }
1560
+ }
1561
+ if (variants[name]) {
1562
+ variantValues.set(name, styleValue)
1563
+ }
1564
+ inlined.set(name, true)
1565
+ return attr
1566
+ }
1567
+
1568
+ // ternaries!
1569
+
1570
+ // binary ternary, we can eventually make this smarter but step 1
1571
+ // basically for the common use case of:
1572
+ // opacity={(conditional ? 0 : 1) * scale}
1573
+ if (t.isBinaryExpression(value)) {
1574
+ if (shouldPrintDebug) {
1575
+ logger.info(` binary expression ${name} = ${value}`)
1576
+ }
1577
+ const { operator, left, right } = value
1578
+ // if one side is a ternary, and the other side is evaluatable, we can maybe extract
1579
+ const lVal = attemptEvalSafe(left)
1580
+ const rVal = attemptEvalSafe(right)
1581
+ if (shouldPrintDebug) {
1582
+ logger.info(
1583
+ ` evalBinaryExpression lVal ${String(lVal)}, rVal ${String(rVal)}`
1584
+ )
1585
+ }
1586
+ if (lVal !== FAILED_EVAL && t.isConditionalExpression(right)) {
1587
+ const ternary = addBinaryConditional(operator, left, right)
1588
+ if (ternary) return ternary
1589
+ }
1590
+ if (rVal !== FAILED_EVAL && t.isConditionalExpression(left)) {
1591
+ const ternary = addBinaryConditional(operator, right, left)
1592
+ if (ternary) return ternary
1593
+ }
1594
+ if (shouldPrintDebug) {
1595
+ logger.info(` evalBinaryExpression cant extract`)
1596
+ }
1597
+ inlined.set(name, true)
1598
+ return attr
1599
+ }
1600
+
1601
+ const staticConditional = getStaticConditional(value)
1602
+ if (staticConditional) {
1603
+ if (shouldPrintDebug === 'verbose') {
1604
+ logger.info(` static conditional ${name} ${value}`)
1605
+ }
1606
+ return { type: 'ternary', value: staticConditional }
1607
+ }
1608
+
1609
+ const staticLogical = getStaticLogical(value)
1610
+ if (staticLogical) {
1611
+ if (shouldPrintDebug === 'verbose') {
1612
+ logger.info(` static ternary ${name} = ${value}`)
1613
+ }
1614
+ return { type: 'ternary', value: staticLogical }
1615
+ }
1616
+
1617
+ // if we've made it this far, the prop stays inline
1618
+ inlined.set(name, true)
1619
+ if (shouldPrintDebug) {
1620
+ logger.info(` ! inline no match ${name} ${value}`)
1621
+ }
1622
+
1623
+ //
1624
+ // RETURN ATTR
1625
+ //
1626
+ return attr
1627
+
1628
+ // attr helpers:
1629
+ function addBinaryConditional(
1630
+ operator: any,
1631
+ staticExpr: any,
1632
+ cond: t.ConditionalExpression
1633
+ ): ExtractedAttr | null {
1634
+ if (getStaticConditional(cond)) {
1635
+ const alt = attemptEval(
1636
+ t.binaryExpression(operator, staticExpr, cond.alternate)
1637
+ )
1638
+ const cons = attemptEval(
1639
+ t.binaryExpression(operator, staticExpr, cond.consequent)
1640
+ )
1641
+ if (shouldPrintDebug) {
1642
+ logger.info([' binaryConditional', cond.test, cons, alt].join(' '))
1643
+ }
1644
+ return {
1645
+ type: 'ternary',
1646
+ value: {
1647
+ test: cond.test,
1648
+ remove,
1649
+ alternate: { [name]: alt },
1650
+ consequent: { [name]: cons },
1651
+ },
1652
+ }
1653
+ }
1654
+ return null
1655
+ }
1656
+
1657
+ function getStaticConditional(value: t.Node): Ternary | null {
1658
+ if (t.isConditionalExpression(value)) {
1659
+ try {
1660
+ const aVal = attemptEval(value.alternate)
1661
+ const cVal = attemptEval(value.consequent)
1662
+ if (shouldPrintDebug) {
1663
+ const type = value.test.type
1664
+ logger.info([' static ternary', type, cVal, aVal].join(' '))
1665
+ }
1666
+ return {
1667
+ test: value.test,
1668
+ remove,
1669
+ consequent: { [name]: cVal },
1670
+ alternate: { [name]: aVal },
1671
+ }
1672
+ } catch (err: any) {
1673
+ if (shouldPrintDebug) {
1674
+ logger.info([' cant eval ternary', err.message].join(' '))
1675
+ }
1676
+ }
1677
+ }
1678
+ return null
1679
+ }
1680
+
1681
+ function getStaticLogical(value: t.Node): Ternary | null {
1682
+ if (t.isLogicalExpression(value)) {
1683
+ if (value.operator === '&&') {
1684
+ try {
1685
+ const val = attemptEval(value.right)
1686
+ if (shouldPrintDebug) {
1687
+ logger.info([' staticLogical', value.left, name, val].join(' '))
1688
+ }
1689
+ return {
1690
+ test: value.left,
1691
+ remove,
1692
+ consequent: { [name]: val },
1693
+ alternate: null,
1694
+ }
1695
+ } catch (err) {
1696
+ if (shouldPrintDebug) {
1697
+ logger.info([' cant static eval logical', err].join(' '))
1698
+ }
1699
+ }
1700
+ }
1701
+ }
1702
+ return null
1703
+ }
1704
+ } // END function evaluateAttribute
1705
+
1706
+ function isStaticObject(obj: t.Node): obj is t.ObjectExpression {
1707
+ return (
1708
+ t.isObjectExpression(obj) &&
1709
+ obj.properties.every((prop) => {
1710
+ if (!t.isObjectProperty(prop)) {
1711
+ // console.warn('not an object prop?', prop)
1712
+ return false
1713
+ }
1714
+ const propName = prop.key['name']
1715
+ if (!isValidStyleKey(propName, staticConfig) && propName !== 'render') {
1716
+ if (shouldPrintDebug) {
1717
+ logger.info([' not a valid style prop!', propName].join(' '))
1718
+ }
1719
+ return false
1720
+ }
1721
+ return true
1722
+ })
1723
+ )
1724
+ }
1725
+
1726
+ // side = {
1727
+ // color: 'red',
1728
+ // background: x ? 'red' : 'green',
1729
+ // $gtSm: { color: 'green' }
1730
+ // }
1731
+ // => Ternary<test, { color: 'red' }, null>
1732
+ // => Ternary<test && x, { background: 'red' }, null>
1733
+ // => Ternary<test && !x, { background: 'green' }, null>
1734
+ // => Ternary<test && '$gtSm', { color: 'green' }, null>
1735
+ function flattenNestedTernaries(
1736
+ test: t.Expression,
1737
+ side: t.Expression | null,
1738
+ ternaryPartial: Partial<Ternary> = {}
1739
+ ): null | Ternary[] {
1740
+ if (!side) {
1741
+ return null
1742
+ }
1743
+ if (!isStaticObject(side)) {
1744
+ throw new Error('not extractable')
1745
+ }
1746
+ return side.properties.flatMap((property) => {
1747
+ if (!t.isObjectProperty(property)) {
1748
+ throw new Error('expected object property')
1749
+ }
1750
+ // this could be a recurse here if we want to get fancy
1751
+ if (t.isConditionalExpression(property.value)) {
1752
+ // merge up into the parent conditional, split into two
1753
+ const [truthy, falsy] = [
1754
+ t.objectExpression([
1755
+ t.objectProperty(property.key, property.value.consequent),
1756
+ ]),
1757
+ t.objectExpression([
1758
+ t.objectProperty(property.key, property.value.alternate),
1759
+ ]),
1760
+ ].map((x) => attemptEval(x))
1761
+ return [
1762
+ createTernary({
1763
+ remove() {},
1764
+ ...ternaryPartial,
1765
+ test: t.logicalExpression('&&', test, property.value.test),
1766
+ consequent: truthy,
1767
+ alternate: null,
1768
+ }),
1769
+ createTernary({
1770
+ ...ternaryPartial,
1771
+ test: t.logicalExpression(
1772
+ '&&',
1773
+ test,
1774
+ t.unaryExpression('!', property.value.test)
1775
+ ),
1776
+ consequent: falsy,
1777
+ alternate: null,
1778
+ remove() {},
1779
+ }),
1780
+ ]
1781
+ }
1782
+ const obj = t.objectExpression([
1783
+ t.objectProperty(property.key, property.value),
1784
+ ])
1785
+ const consequent = attemptEval(obj)
1786
+ return createTernary({
1787
+ remove() {},
1788
+ ...ternaryPartial,
1789
+ test,
1790
+ consequent,
1791
+ alternate: null,
1792
+ })
1793
+ })
1794
+ }
1795
+
1796
+ if (couldntParse || shouldDeopt) {
1797
+ if (shouldPrintDebug) {
1798
+ logger.info(
1799
+ [` avoid optimizing:`, { couldntParse, shouldDeopt }].join(' ')
1800
+ )
1801
+ }
1802
+ node.attributes = ogAttributes
1803
+ return
1804
+ }
1805
+
1806
+ // before deopt, can still optimize
1807
+ const parentFn = findTopmostFunction(traversePath)
1808
+ if (parentFn) {
1809
+ modifiedComponents.add(parentFn)
1810
+ }
1811
+
1812
+ // flatten logic!
1813
+ // fairly simple check to see if all children are text
1814
+ const hasSpread = attrs.some(
1815
+ (x) => x.type === 'attr' && t.isJSXSpreadAttribute(x.value)
1816
+ )
1817
+
1818
+ const hasOnlyStringChildren =
1819
+ !hasSpread &&
1820
+ (node.selfClosing ||
1821
+ (traversePath.node.children &&
1822
+ traversePath.node.children.every((x) => x.type === 'JSXText')))
1823
+
1824
+ let themeVal = inlined.get('theme')
1825
+
1826
+ // on native we can't flatten when theme prop is set
1827
+ if (platform !== 'native') {
1828
+ inlined.delete('theme')
1829
+ }
1830
+
1831
+ for (const [key] of inlined) {
1832
+ const isStaticObjectVariant =
1833
+ staticConfig.variants?.[key] && variantValues.has(key)
1834
+ if (INLINE_EXTRACTABLE[key] || isStaticObjectVariant) {
1835
+ inlined.delete(key)
1836
+ }
1837
+ }
1838
+
1839
+ const canFlattenProps = inlined.size === 0
1840
+
1841
+ let shouldFlatten = Boolean(
1842
+ flatNodeName &&
1843
+ !shouldDeopt &&
1844
+ canFlattenProps &&
1845
+ !hasSpread &&
1846
+ !staticConfig.isStyledHOC &&
1847
+ !staticConfig.isHOC &&
1848
+ !staticConfig.isReactNative &&
1849
+ staticConfig.neverFlatten !== true &&
1850
+ (staticConfig.neverFlatten === 'jsx' ? hasOnlyStringChildren : true)
1851
+ )
1852
+
1853
+ const usedThemeKeys = new Set<string>()
1854
+ // if it accesses any theme values during evaluation
1855
+ themeAccessListeners.add((key) => {
1856
+ if (disableExtractVariables) {
1857
+ usedThemeKeys.add(key)
1858
+ shouldFlatten = false
1859
+ if (shouldPrintDebug === 'verbose') {
1860
+ logger.info([' ! accessing theme key, avoid flatten', key].join(' '))
1861
+ }
1862
+ }
1863
+ })
1864
+
1865
+ if (!shouldFlatten) {
1866
+ // were no longer partially optimizing, it adds a lot of complexity for dubious performance
1867
+ if (shouldPrintDebug) {
1868
+ logger.info(
1869
+ `Deopting ${JSON.stringify({
1870
+ shouldFlatten,
1871
+ shouldDeopt,
1872
+ canFlattenProps,
1873
+ hasSpread,
1874
+ neverFlatten: staticConfig.neverFlatten,
1875
+ })}`
1876
+ )
1877
+ }
1878
+ node.attributes = ogAttributes
1879
+ return
1880
+ }
1881
+
1882
+ // ensure the default styles are there
1883
+ let skipMap = false
1884
+ const defaultStyleAttrs = Object.keys(defaultProps).flatMap((key) => {
1885
+ if (skipMap) return []
1886
+ const value = defaultProps[key]
1887
+ if (key === 'theme' && !themeVal) {
1888
+ if (platform === 'native') {
1889
+ shouldFlatten = false
1890
+ skipMap = true
1891
+ inlined.set('theme', { value: t.stringLiteral(value) })
1892
+ }
1893
+ themeVal = { value: t.stringLiteral(value) }
1894
+ return []
1895
+ }
1896
+ if (!isValidStyleKey(key, staticConfig)) {
1897
+ return []
1898
+ }
1899
+ const name = guiConfig?.shorthands[key] || key
1900
+ if (value === undefined) {
1901
+ logger.warn(
1902
+ `⚠️ Error evaluating default style for component, prop ${key} ${value}`
1903
+ )
1904
+ shouldDeopt = true
1905
+ return
1906
+ }
1907
+ if (name[0] === '$' && mediaQueryConfig[name.slice(1)]) {
1908
+ defaultProps[key] = undefined
1909
+ return evaluateAttribute({
1910
+ node: t.jsxAttribute(
1911
+ t.jsxIdentifier(name),
1912
+ t.jsxExpressionContainer(
1913
+ t.objectExpression(
1914
+ Object.keys(value)
1915
+ .filter((k) => {
1916
+ return typeof value[k] !== 'undefined'
1917
+ })
1918
+ .map((k) => {
1919
+ return t.objectProperty(t.identifier(k), literalToAst(value[k]))
1920
+ })
1921
+ )
1922
+ )
1923
+ ),
1924
+ } as any)
1925
+ }
1926
+ const attr: ExtractedAttrStyle = {
1927
+ type: 'style',
1928
+ name,
1929
+ value: { [name]: value },
1930
+ }
1931
+ return attr
1932
+ }) as ExtractedAttr[]
1933
+
1934
+ if (!skipMap) {
1935
+ if (defaultStyleAttrs.length) {
1936
+ attrs = [...defaultStyleAttrs, ...attrs]
1937
+ }
1938
+ }
1939
+
1940
+ // combine ternaries
1941
+ let ternaries: Ternary[] = []
1942
+ attrs = attrs
1943
+ .reduce<(ExtractedAttr | ExtractedAttr[])[]>((out, cur) => {
1944
+ const next = attrs[attrs.indexOf(cur) + 1]
1945
+ if (cur.type === 'ternary') {
1946
+ ternaries.push(cur.value)
1947
+ }
1948
+ if ((!next || next.type !== 'ternary') && ternaries.length) {
1949
+ // finish, process
1950
+ const normalized = normalizeTernaries(ternaries).map(
1951
+ ({ alternate, consequent, ...rest }) => {
1952
+ return {
1953
+ type: 'ternary' as const,
1954
+ value: {
1955
+ ...rest,
1956
+ alternate: alternate || null,
1957
+ consequent: consequent || null,
1958
+ },
1959
+ }
1960
+ }
1961
+ )
1962
+ try {
1963
+ return [...out, ...normalized]
1964
+ } finally {
1965
+ if (shouldPrintDebug) {
1966
+ logger.info(
1967
+ ` normalizeTernaries (${ternaries.length} => ${normalized.length})`
1968
+ )
1969
+ }
1970
+ ternaries = []
1971
+ }
1972
+ }
1973
+ if (cur.type === 'ternary') {
1974
+ return out
1975
+ }
1976
+ out.push(cur)
1977
+ return out
1978
+ }, [])
1979
+ .flat()
1980
+
1981
+ // wrap theme around children on flatten
1982
+ // account for shouldFlatten could change w the above block "if (disableExtractVariables)"
1983
+ if (themeVal) {
1984
+ if (!programPath) {
1985
+ console.warn(
1986
+ `No program path found, avoiding importing flattening / importing theme in ${sourcePath}`
1987
+ )
1988
+ } else {
1989
+ if (shouldPrintDebug) {
1990
+ logger.info([' - wrapping theme', themeVal].join(' '))
1991
+ }
1992
+
1993
+ // remove theme attribute from flattened node
1994
+ attrs = attrs.filter(
1995
+ (x) =>
1996
+ !(
1997
+ x.type === 'attr' &&
1998
+ t.isJSXAttribute(x.value) &&
1999
+ x.value.name.name === 'theme'
2000
+ )
2001
+ )
2002
+
2003
+ // add import
2004
+ if (!hasImportedTheme) {
2005
+ hasImportedTheme = true
2006
+ programPath.node.body.push(
2007
+ t.importDeclaration(
2008
+ [
2009
+ t.importSpecifier(
2010
+ t.identifier('_GuiTheme'),
2011
+ t.identifier('Theme')
2012
+ ),
2013
+ ],
2014
+ t.stringLiteral('@hanzogui/web')
2015
+ )
2016
+ )
2017
+ }
2018
+
2019
+ traversePath.replaceWith(
2020
+ t.jsxElement(
2021
+ t.jsxOpeningElement(t.jsxIdentifier('_GuiTheme'), [
2022
+ t.jsxAttribute(t.jsxIdentifier('name'), themeVal.value),
2023
+ ]),
2024
+ t.jsxClosingElement(t.jsxIdentifier('_GuiTheme')),
2025
+ [traversePath.node]
2026
+ )
2027
+ )
2028
+ }
2029
+ }
2030
+
2031
+ if (shouldPrintDebug) {
2032
+ logger.info(
2033
+ [' - attrs (flattened): \n', logLines(attrs.map(attrStr).join(', '))].join(
2034
+ ' '
2035
+ )
2036
+ )
2037
+ }
2038
+
2039
+ function mergeToEnd(obj: object, key: string, val: any) {
2040
+ if (key in obj) {
2041
+ delete obj[key]
2042
+ }
2043
+ obj[key] = val
2044
+ }
2045
+
2046
+ // preserves order
2047
+ function normalizeStyleWithoutVariants(style: any) {
2048
+ let res = {}
2049
+ for (const key in style) {
2050
+ if (staticConfig.variants && key in staticConfig.variants) {
2051
+ mergeToEnd(res, key, style[key])
2052
+ } else {
2053
+ const expanded = normalizeStyle({ [key]: style[key] }, true)
2054
+ for (const key in expanded) {
2055
+ mergeToEnd(res, key, expanded[key])
2056
+ }
2057
+ }
2058
+ }
2059
+ return res
2060
+ }
2061
+
2062
+ // evaluates all static attributes into a simple object
2063
+ let foundStaticProps = {}
2064
+
2065
+ for (const key in attrs) {
2066
+ const cur = attrs[key]
2067
+ if (cur.type === 'style') {
2068
+ // remove variants because they are processed later, and can lead to invalid values here
2069
+ // see <Spacer flex /> where flex looks like a valid style, but is a variant
2070
+ const expanded = normalizeStyleWithoutVariants(cur.value)
2071
+ // preserve order
2072
+ for (const key in expanded) {
2073
+ mergeToEnd(foundStaticProps, key, expanded[key])
2074
+ }
2075
+ continue
2076
+ }
2077
+ if (cur.type === 'attr') {
2078
+ if (t.isJSXSpreadAttribute(cur.value)) {
2079
+ continue
2080
+ }
2081
+ if (!t.isJSXIdentifier(cur.value.name)) {
2082
+ continue
2083
+ }
2084
+ const key = cur.value.name.name
2085
+ // undefined = boolean true
2086
+ const value = attemptEvalSafe(cur.value.value || t.booleanLiteral(true))
2087
+ if (value !== FAILED_EVAL) {
2088
+ mergeToEnd(foundStaticProps, key, value)
2089
+ }
2090
+ }
2091
+ }
2092
+
2093
+ // must preserve exact order
2094
+ const completeProps = {}
2095
+ for (const key in defaultProps) {
2096
+ if (!(key in foundStaticProps)) {
2097
+ completeProps[key] = defaultProps[key]
2098
+ }
2099
+ }
2100
+ for (const key in foundStaticProps) {
2101
+ completeProps[key] = foundStaticProps[key]
2102
+ }
2103
+
2104
+ // expand shorthands, de-opt variables
2105
+ attrs = attrs.reduce<ExtractedAttr[]>((acc, cur) => {
2106
+ if (!cur) return acc
2107
+ if (cur.type === 'attr' && !t.isJSXSpreadAttribute(cur.value)) {
2108
+ if (shouldFlatten) {
2109
+ const name = cur.value.name.name
2110
+ if (typeof name === 'string') {
2111
+ if (name === 'render') {
2112
+ // remove render=""
2113
+ return acc
2114
+ }
2115
+
2116
+ // if flattening, expand variants
2117
+ if (variants[name] && variantValues.has(name)) {
2118
+ const styleState = {
2119
+ ...propMapperStyleState,
2120
+ props: completeProps,
2121
+ }
2122
+
2123
+ let out: Record<string, any> = {}
2124
+ propMapper(
2125
+ name,
2126
+ variantValues.get(name),
2127
+ styleState,
2128
+ false,
2129
+ (key, val) => {
2130
+ out[key] = val
2131
+ }
2132
+ )
2133
+
2134
+ if (out && isTargetingHTML) {
2135
+ const cn = out.className
2136
+ // translate to DOM-compat
2137
+ out = reactNativeWebInternals.createDOMProps(
2138
+ isTextView ? 'span' : 'div',
2139
+ out
2140
+ )
2141
+ // remove rnw className use ours
2142
+ out.className = cn
2143
+ }
2144
+ if (shouldPrintDebug) {
2145
+ logger.info([' - expanded variant', name, out].join(' '))
2146
+ }
2147
+ for (const key in out) {
2148
+ const value = out[key]
2149
+ if (isValidStyleKey(key, staticConfig)) {
2150
+ acc.push({
2151
+ type: 'style',
2152
+ value: { [key]: value },
2153
+ name: key,
2154
+ attr: cur.value,
2155
+ } as const)
2156
+ } else {
2157
+ acc.push({
2158
+ type: 'attr',
2159
+ value: t.jsxAttribute(
2160
+ t.jsxIdentifier(key),
2161
+ t.jsxExpressionContainer(
2162
+ typeof value === 'string'
2163
+ ? t.stringLiteral(value)
2164
+ : literalToAst(value)
2165
+ )
2166
+ ),
2167
+ })
2168
+ }
2169
+ }
2170
+ }
2171
+ }
2172
+ }
2173
+ }
2174
+
2175
+ if (cur.type !== 'style') {
2176
+ acc.push(cur)
2177
+ return acc
2178
+ }
2179
+
2180
+ let key = Object.keys(cur.value)[0]
2181
+ const value = cur.value[key]
2182
+ const fullKey = guiConfig?.shorthands[key]
2183
+ // expand shorthands
2184
+ if (fullKey) {
2185
+ cur.value = { [fullKey]: value }
2186
+ key = fullKey
2187
+ }
2188
+
2189
+ // finally we have all styles + expansions, lets see if we need to skip
2190
+ // any and keep them as attrs
2191
+ if (disableExtractVariables) {
2192
+ if (
2193
+ value[0] === '$' &&
2194
+ (usedThemeKeys.has(key) || usedThemeKeys.has(fullKey))
2195
+ ) {
2196
+ if (shouldPrintDebug) {
2197
+ logger.info([` keeping variable inline: ${key} =`, value].join(' '))
2198
+ }
2199
+ acc.push({
2200
+ type: 'attr',
2201
+ value: t.jsxAttribute(
2202
+ t.jsxIdentifier(key),
2203
+ t.jsxExpressionContainer(t.stringLiteral(value))
2204
+ ),
2205
+ })
2206
+ return acc
2207
+ }
2208
+ }
2209
+
2210
+ acc.push(cur)
2211
+ return acc
2212
+ }, [])
2213
+
2214
+ tm.mark('jsx-element-expanded', !!shouldPrintDebug)
2215
+ if (shouldPrintDebug) {
2216
+ logger.info(
2217
+ [' - attrs (expanded): \n', logLines(attrs.map(attrStr).join(', '))].join(
2218
+ ' '
2219
+ )
2220
+ )
2221
+ }
2222
+
2223
+ // merge styles, leave undefined values
2224
+ let prev: ExtractedAttr | null = null
2225
+
2226
+ function mergeStyles(
2227
+ prev: ViewStyle & PseudoStyles,
2228
+ next: ViewStyle & PseudoStyles
2229
+ ) {
2230
+ for (const key in next) {
2231
+ // merge pseudos
2232
+ if (pseudoDescriptors[key]) {
2233
+ prev[key] = prev[key] || {}
2234
+ Object.assign(prev[key], next[key])
2235
+ } else {
2236
+ mergeToEnd(prev, key, next[key])
2237
+ }
2238
+ }
2239
+ }
2240
+
2241
+ // post process
2242
+ const getProps = (
2243
+ props: object | null,
2244
+ includeProps = false,
2245
+ debugName = ''
2246
+ ) => {
2247
+ if (!props) {
2248
+ if (shouldPrintDebug) logger.info([' getProps() no props'].join(' '))
2249
+ return {}
2250
+ }
2251
+ if (excludeProps?.size) {
2252
+ for (const key in props) {
2253
+ if (excludeProps.has(key)) {
2254
+ if (shouldPrintDebug) logger.info([' delete excluded', key].join(' '))
2255
+ delete props[key]
2256
+ }
2257
+ }
2258
+ }
2259
+
2260
+ const before = process.env.IS_STATIC
2261
+ process.env.IS_STATIC = 'is_static'
2262
+ try {
2263
+ const out = getSplitStyles(
2264
+ props,
2265
+ staticConfig,
2266
+ defaultTheme,
2267
+ '',
2268
+ componentState,
2269
+ {
2270
+ ...styleProps,
2271
+ noClass: true,
2272
+ fallbackProps: completeProps,
2273
+ ...(platform === 'native' && {
2274
+ resolveValues: 'except-theme',
2275
+ }),
2276
+ },
2277
+ undefined,
2278
+ undefined,
2279
+ undefined,
2280
+ undefined,
2281
+ false,
2282
+ debugPropValue || shouldPrintDebug
2283
+ )!
2284
+
2285
+ let outProps = {
2286
+ ...(includeProps ? out.viewProps : {}),
2287
+ ...out.style,
2288
+ ...out.pseudos,
2289
+ }
2290
+
2291
+ // check de-opt props again
2292
+ for (const key in outProps) {
2293
+ if (deoptProps.has(key)) {
2294
+ shouldFlatten = false
2295
+ }
2296
+ }
2297
+
2298
+ if (shouldPrintDebug) {
2299
+ logger.info(`(${debugName})`)
2300
+ // prettier-ignore
2301
+ logger.info(`\n getProps (props in): ${logLines(objToStr(props))}`)
2302
+ // prettier-ignore
2303
+ logger.info(
2304
+ `\n getProps (outProps): ${logLines(objToStr(outProps))}`
2305
+ )
2306
+ }
2307
+
2308
+ if (out.fontFamily) {
2309
+ setPropsToFontFamily(outProps, out.fontFamily)
2310
+ if (shouldPrintDebug) {
2311
+ logger.info(`\n 💬 new font fam: ${out.fontFamily}`)
2312
+ }
2313
+ }
2314
+
2315
+ return outProps
2316
+ } catch (err: any) {
2317
+ logger.info(['error', err.message, err.stack].join(' '))
2318
+ return {}
2319
+ } finally {
2320
+ process.env.IS_STATIC = before
2321
+ }
2322
+ }
2323
+
2324
+ // add default props
2325
+ attrs.unshift({
2326
+ type: 'style',
2327
+ value: defaultProps,
2328
+ })
2329
+
2330
+ attrs = attrs.reduce<ExtractedAttr[]>((acc, cur) => {
2331
+ if (cur.type === 'style') {
2332
+ const keys = Object.keys(cur.value || {})
2333
+ if (!keys.length) {
2334
+ return acc
2335
+ }
2336
+ const key = keys[0]
2337
+ const value = cur.value[key]
2338
+
2339
+ // Check if this is a media-like key ($theme-, $platform-, $group-, or $mediaQuery)
2340
+ const isMediaLikeKey =
2341
+ key[0] === '$' &&
2342
+ (key.startsWith('$theme-') ||
2343
+ key.startsWith('$platform-') ||
2344
+ key.startsWith('$group-') ||
2345
+ mediaQueryConfig[key.slice(1)])
2346
+
2347
+ const shouldKeepOriginalAttr =
2348
+ // !isStyleAndAttr[key] &&
2349
+ !shouldFlatten &&
2350
+ // de-opt if non-style
2351
+ !validStyles[key] &&
2352
+ !pseudoDescriptors[key] &&
2353
+ !isMediaLikeKey &&
2354
+ !(key.startsWith('data-') || key.startsWith('aria-'))
2355
+
2356
+ if (shouldKeepOriginalAttr) {
2357
+ if (shouldPrintDebug) {
2358
+ logger.info([' - keeping as non-style', key].join(' '))
2359
+ }
2360
+ prev = cur
2361
+ acc.push({
2362
+ type: 'attr',
2363
+ value: t.jsxAttribute(
2364
+ t.jsxIdentifier(key),
2365
+ t.jsxExpressionContainer(
2366
+ typeof value === 'string'
2367
+ ? t.stringLiteral(value)
2368
+ : literalToAst(value)
2369
+ )
2370
+ ),
2371
+ })
2372
+ acc.push(cur)
2373
+ return acc
2374
+ }
2375
+
2376
+ if (prev?.type === 'style') {
2377
+ mergeStyles(prev.value, cur.value)
2378
+ return acc
2379
+ }
2380
+ }
2381
+
2382
+ if (cur.type === 'style') {
2383
+ prev = cur
2384
+ }
2385
+ acc.push(cur)
2386
+ return acc
2387
+ }, [])
2388
+
2389
+ if (shouldPrintDebug) {
2390
+ logger.info(
2391
+ [
2392
+ ' - attrs (combined 🔀): \n',
2393
+ logLines(attrs.map(attrStr).join(', ')),
2394
+ ].join(' ')
2395
+ )
2396
+ }
2397
+
2398
+ let getStyleError: any = null
2399
+
2400
+ // fix up ternaries, combine final style values
2401
+ for (const attr of attrs) {
2402
+ try {
2403
+ if (shouldPrintDebug) {
2404
+ console.info(` Processing ${attr.type}:`)
2405
+ }
2406
+
2407
+ switch (attr.type) {
2408
+ case 'ternary': {
2409
+ const a = getProps(attr.value.alternate, false, 'ternary.alternate')
2410
+ const c = getProps(attr.value.consequent, false, 'ternary.consequent')
2411
+ if (a) attr.value.alternate = a
2412
+ if (c) attr.value.consequent = c
2413
+ if (shouldPrintDebug)
2414
+ logger.info([' => tern ', attrStr(attr)].join(' '))
2415
+ continue
2416
+ }
2417
+ case 'style': {
2418
+ // expand variants and such
2419
+ const styles = getProps(attr.value, false, 'style')
2420
+ if (styles) {
2421
+ // @ts-ignore
2422
+ attr.value = styles
2423
+ }
2424
+ // prettier-ignore
2425
+ if (shouldPrintDebug)
2426
+ logger.info(
2427
+ [' * styles (in)', logLines(objToStr(attr.value))].join(' ')
2428
+ )
2429
+ // prettier-ignore
2430
+ if (shouldPrintDebug)
2431
+ logger.info(
2432
+ [' * styles (out)', logLines(objToStr(styles))].join(' ')
2433
+ )
2434
+ continue
2435
+ }
2436
+ case 'attr': {
2437
+ if (shouldFlatten && t.isJSXAttribute(attr.value)) {
2438
+ // we know all attributes are static
2439
+ // this only does one at a time but it should really do the whole group together...
2440
+ // also awkward to be doing it using jsxAttributes...
2441
+ const key = attr.value.name.name as string
2442
+
2443
+ // dont process style/className can just stay attrs
2444
+ if (key === 'style' || key === 'className' || key === 'render') {
2445
+ continue
2446
+ }
2447
+
2448
+ // undefined = boolean true
2449
+ const value = attemptEvalSafe(
2450
+ attr.value.value || t.booleanLiteral(true)
2451
+ )
2452
+ if (value !== FAILED_EVAL) {
2453
+ const outProps = getProps({ [key]: value }, true, `attr.${key}`)
2454
+ const outKey = Object.keys(outProps)[0]
2455
+ if (outKey) {
2456
+ const outVal = outProps[outKey]
2457
+ attr.value = t.jsxAttribute(
2458
+ t.jsxIdentifier(outKey),
2459
+ t.jsxExpressionContainer(
2460
+ typeof outVal === 'string'
2461
+ ? t.stringLiteral(outVal)
2462
+ : literalToAst(outVal)
2463
+ )
2464
+ )
2465
+ }
2466
+ }
2467
+ }
2468
+ }
2469
+ }
2470
+ } catch (err) {
2471
+ // any error de-opt
2472
+ getStyleError = err
2473
+ }
2474
+ }
2475
+
2476
+ if (shouldPrintDebug) {
2477
+ // prettier-ignore
2478
+ logger.info(
2479
+ [
2480
+ ' - attrs (ternaries/combined):\n',
2481
+ logLines(attrs.map(attrStr).join(', ')),
2482
+ ].join(' ')
2483
+ )
2484
+ }
2485
+
2486
+ tm.mark('jsx-element-styles', !!shouldPrintDebug)
2487
+
2488
+ if (getStyleError) {
2489
+ logger.info([' ⚠️ postprocessing error, deopt', getStyleError].join(' '))
2490
+ node.attributes = ogAttributes
2491
+ return null
2492
+ }
2493
+
2494
+ // final lazy extra loop:
2495
+ const existingStyleKeys = new Set()
2496
+ for (let i = attrs.length - 1; i >= 0; i--) {
2497
+ const attr = attrs[i]
2498
+
2499
+ // if flattening map inline props to proper flattened names
2500
+ if (shouldFlatten) {
2501
+ if (attr.type === 'attr') {
2502
+ if (t.isJSXAttribute(attr.value)) {
2503
+ if (t.isJSXIdentifier(attr.value.name)) {
2504
+ const name = attr.value.name.name
2505
+ if (INLINE_EXTRACTABLE[name]) {
2506
+ // map to HTML only name
2507
+ attr.value.name.name = INLINE_EXTRACTABLE[name]
2508
+ }
2509
+ }
2510
+ }
2511
+ }
2512
+ }
2513
+
2514
+ // remove duplicate styles
2515
+ // so if you have:
2516
+ // style({ color: 'red' }), ...someProps, style({ color: 'green' })
2517
+ // this will mutate:
2518
+ // style({}), ...someProps, style({ color: 'green' })
2519
+ if (attr.type === 'style') {
2520
+ for (const key in attr.value) {
2521
+ if (existingStyleKeys.has(key)) {
2522
+ if (shouldPrintDebug) {
2523
+ logger.info([` >> delete existing ${key}`].join(' '))
2524
+ }
2525
+ delete attr.value[key]
2526
+ } else {
2527
+ existingStyleKeys.add(key)
2528
+ }
2529
+ }
2530
+ }
2531
+ }
2532
+
2533
+ attrs = attrs.filter(Boolean)
2534
+
2535
+ // inlineWhenUnflattened
2536
+ if (!shouldFlatten) {
2537
+ if (inlineWhenUnflattened.size) {
2538
+ for (const [index, attr] of attrs.entries()) {
2539
+ if (attr.type === 'style') {
2540
+ for (const key in attr.value) {
2541
+ if (!inlineWhenUnflattened.has(key)) continue
2542
+ const val = inlineWhenUnflattenedOGVals[key]
2543
+ if (val) {
2544
+ // delete the style
2545
+ delete attr.value[key]
2546
+
2547
+ // and insert it before
2548
+ attrs.splice(index - 1, 0, val.attr)
2549
+ } else {
2550
+ // just delete it, it was added during expansion but should be left inline
2551
+ delete attr.value[key]
2552
+ }
2553
+ }
2554
+ }
2555
+ }
2556
+ }
2557
+ }
2558
+
2559
+ // delete empty styles:
2560
+ attrs = attrs.filter((x) => {
2561
+ if (x.type === 'style' && Object.keys(x.value).length === 0) {
2562
+ return false
2563
+ }
2564
+ return true
2565
+ })
2566
+
2567
+ const isNativeNotFlat = !shouldFlatten && platform === 'native'
2568
+ if (isNativeNotFlat) {
2569
+ if (shouldPrintDebug) {
2570
+ logger.info(
2571
+ `Disabled flattening except for simple cases on native for now: ${JSON.stringify(
2572
+ {
2573
+ flatNode: flatNodeName,
2574
+ shouldDeopt,
2575
+ canFlattenProps,
2576
+ hasSpread,
2577
+ 'staticConfig.isStyledHOC': staticConfig.isStyledHOC,
2578
+ '!staticConfig.isHOC': !staticConfig.isHOC,
2579
+ 'staticConfig.isReactNative': staticConfig.isReactNative,
2580
+ 'staticConfig.neverFlatten': staticConfig.neverFlatten,
2581
+ },
2582
+ null,
2583
+ 2
2584
+ )}`
2585
+ )
2586
+ }
2587
+ node.attributes = ogAttributes
2588
+ return null
2589
+ }
2590
+
2591
+ if (shouldPrintDebug) {
2592
+ // prettier-ignore
2593
+ logger.info(
2594
+ [
2595
+ ` - inlined props (${inlined.size}):`,
2596
+ shouldDeopt ? ' deopted' : '',
2597
+ hasSpread ? ' has spread' : '',
2598
+ staticConfig.neverFlatten ? 'neverFlatten' : '',
2599
+ ].join(' ')
2600
+ )
2601
+ logger.info(` - attrs (end):\n ${logLines(attrs.map(attrStr).join(', '))}`)
2602
+ }
2603
+
2604
+ onExtractTag({
2605
+ parserProps: propsWithFileInfo,
2606
+ attrs,
2607
+ node,
2608
+ lineNumbers,
2609
+ filePath,
2610
+ config: guiConfig!,
2611
+ flatNodeName,
2612
+ attemptEval,
2613
+ jsxPath: traversePath,
2614
+ originalNodeName,
2615
+ programPath: programPath!,
2616
+ completeProps,
2617
+ staticConfig,
2618
+ })
2619
+
2620
+ if (shouldFlatten) {
2621
+ if (shouldPrintDebug) {
2622
+ logger.info([' [✅] flattened', originalNodeName, flatNodeName].join(' '))
2623
+ }
2624
+ // Only rename if onExtractTag hasn't already renamed to a custom wrapper
2625
+ // @ts-ignore - check if already renamed by callback (e.g., to a styled wrapper)
2626
+ const currentName = node.name?.name
2627
+ if (
2628
+ !currentName ||
2629
+ currentName === originalNodeName ||
2630
+ currentName.startsWith('__ReactNative')
2631
+ ) {
2632
+ // @ts-ignore
2633
+ node.name.name = flatNodeName
2634
+ if (closingElement) {
2635
+ // @ts-ignore
2636
+ closingElement.name.name = flatNodeName
2637
+ }
2638
+ }
2639
+ res.flattened++
2640
+ }
2641
+ } catch (err: any) {
2642
+ node.attributes = ogAttributes
2643
+
2644
+ if (!(err instanceof BailOptimizationError)) {
2645
+ console.error(
2646
+ `@hanzogui/static error, reverting optimization. In ${filePath} ${lineNumbers} on ${originalNodeName}: ${err.message}. For stack trace set environment HANZO_GUI_DEBUG=1`
2647
+ )
2648
+ if (process.env.HANZO_GUI_DEBUG === '1') {
2649
+ console.error(err.stack)
2650
+ }
2651
+ }
2652
+ } finally {
2653
+ if (debugPropValue) {
2654
+ shouldPrintDebug = ogDebug
2655
+ }
2656
+ }
2657
+ },
2658
+ })
2659
+
2660
+ tm.mark('jsx-done', !!shouldPrintDebug)
2661
+ tm.done(shouldPrintDebug === 'verbose')
2662
+
2663
+ return res
2664
+ }
2665
+ }