@tamagui/static 2.7.7 → 3.0.0-beta.1093.1

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 (335) hide show
  1. package/dist/cjs/check-dep-versions.cjs +354 -0
  2. package/dist/cjs/checkDeps.cjs +301 -0
  3. package/dist/cjs/compiler.cjs +349 -0
  4. package/dist/cjs/compilerHost.cjs +2285 -0
  5. package/dist/cjs/componentDiscovery.cjs +94 -0
  6. package/dist/cjs/constants.cjs +32 -0
  7. package/dist/cjs/domStructuralPass.cjs +121 -0
  8. package/dist/cjs/exports.cjs +44 -0
  9. package/dist/cjs/extractor/addLocalExports.cjs +63 -0
  10. package/dist/cjs/extractor/bundle.cjs +229 -0
  11. package/dist/cjs/extractor/bundleConfig.cjs +769 -0
  12. package/dist/cjs/extractor/concatClassName.cjs +120 -0
  13. package/dist/cjs/extractor/detectModuleFormat.cjs +53 -0
  14. package/dist/cjs/extractor/esbuildAliasPlugin.cjs +46 -0
  15. package/dist/cjs/extractor/esbuildTsconfigPaths.cjs +91 -0
  16. package/dist/cjs/extractor/getTamaguiConfigPathFromOptionsConfig.cjs +42 -0
  17. package/dist/cjs/extractor/loadTamagui.cjs +394 -0
  18. package/dist/cjs/extractor/regenerateConfig.cjs +160 -0
  19. package/dist/cjs/extractor/watchTamaguiConfig.cjs +63 -0
  20. package/dist/cjs/getPragmaOptions.cjs +66 -0
  21. package/dist/cjs/helpers/memoize.cjs +41 -0
  22. package/dist/cjs/helpers/requireTamaguiCore.cjs +39 -0
  23. package/dist/cjs/index.cjs +33 -0
  24. package/dist/cjs/registerRequire.cjs +219 -0
  25. package/dist/cjs/server.cjs +65 -0
  26. package/dist/cjs/staticEvaluationIgnoredModules.cjs +85 -0
  27. package/dist/cjs/types.cjs +16 -0
  28. package/dist/cjs/zero/artifact.cjs +144 -0
  29. package/dist/cjs/zero/graph.cjs +208 -0
  30. package/dist/cjs/zero/identity.cjs +60 -0
  31. package/dist/cjs/zero/index.cjs +33 -0
  32. package/dist/cjs/zero/islands.cjs +234 -0
  33. package/dist/cjs/zero/options.cjs +162 -0
  34. package/dist/cjs/zero/ownership.cjs +78 -0
  35. package/dist/cjs/zero/theme.cjs +337 -0
  36. package/dist/cjs/zero/transform.cjs +207 -0
  37. package/dist/esm/check-dep-versions.mjs +329 -0
  38. package/dist/esm/check-dep-versions.mjs.map +1 -0
  39. package/dist/esm/checkDeps.mjs +279 -0
  40. package/dist/esm/checkDeps.mjs.map +1 -0
  41. package/dist/esm/compiler.mjs +319 -0
  42. package/dist/esm/compiler.mjs.map +1 -0
  43. package/dist/esm/compilerHost.mjs +2263 -0
  44. package/dist/esm/compilerHost.mjs.map +1 -0
  45. package/dist/esm/componentDiscovery.mjs +69 -0
  46. package/dist/esm/componentDiscovery.mjs.map +1 -0
  47. package/dist/esm/constants.mjs +7 -0
  48. package/dist/esm/constants.mjs.map +1 -0
  49. package/dist/esm/domStructuralPass.mjs +102 -0
  50. package/dist/esm/domStructuralPass.mjs.map +1 -0
  51. package/dist/esm/exports.mjs +29 -0
  52. package/dist/esm/extractor/addLocalExports.mjs +44 -0
  53. package/dist/esm/extractor/addLocalExports.mjs.map +1 -0
  54. package/dist/esm/extractor/bundle.mjs +199 -0
  55. package/dist/esm/extractor/bundle.mjs.map +1 -0
  56. package/dist/esm/extractor/bundleConfig.mjs +728 -0
  57. package/dist/esm/extractor/bundleConfig.mjs.map +1 -0
  58. package/dist/esm/extractor/concatClassName.mjs +100 -0
  59. package/dist/esm/extractor/concatClassName.mjs.map +1 -0
  60. package/dist/esm/extractor/detectModuleFormat.mjs +31 -0
  61. package/dist/esm/extractor/detectModuleFormat.mjs.map +1 -0
  62. package/dist/esm/extractor/esbuildAliasPlugin.mjs +26 -0
  63. package/dist/esm/extractor/esbuildAliasPlugin.mjs.map +1 -0
  64. package/dist/esm/extractor/esbuildTsconfigPaths.mjs +63 -0
  65. package/dist/esm/extractor/esbuildTsconfigPaths.mjs.map +1 -0
  66. package/dist/esm/extractor/getTamaguiConfigPathFromOptionsConfig.mjs +23 -0
  67. package/dist/esm/extractor/getTamaguiConfigPathFromOptionsConfig.mjs.map +1 -0
  68. package/dist/esm/extractor/loadTamagui.mjs +357 -0
  69. package/dist/esm/extractor/loadTamagui.mjs.map +1 -0
  70. package/dist/esm/extractor/regenerateConfig.mjs +131 -0
  71. package/dist/esm/extractor/regenerateConfig.mjs.map +1 -0
  72. package/dist/esm/extractor/watchTamaguiConfig.mjs +43 -0
  73. package/dist/esm/extractor/watchTamaguiConfig.mjs.map +1 -0
  74. package/dist/esm/getPragmaOptions.mjs +46 -0
  75. package/dist/esm/getPragmaOptions.mjs.map +1 -0
  76. package/dist/esm/helpers/memoize.mjs +21 -0
  77. package/dist/esm/helpers/memoize.mjs.map +1 -0
  78. package/dist/esm/helpers/requireTamaguiCore.mjs +19 -0
  79. package/dist/esm/helpers/requireTamaguiCore.mjs.map +1 -0
  80. package/dist/esm/index.mjs +8 -0
  81. package/dist/esm/index.mjs.map +1 -0
  82. package/dist/esm/registerRequire.mjs +195 -0
  83. package/dist/esm/registerRequire.mjs.map +1 -0
  84. package/dist/esm/server.mjs +40 -0
  85. package/dist/esm/server.mjs.map +1 -0
  86. package/dist/esm/setup.mjs +0 -0
  87. package/dist/esm/staticEvaluationIgnoredModules.mjs +61 -0
  88. package/dist/esm/staticEvaluationIgnoredModules.mjs.map +1 -0
  89. package/dist/esm/types.mjs +0 -0
  90. package/dist/esm/zero/artifact.mjs +119 -0
  91. package/dist/esm/zero/artifact.mjs.map +1 -0
  92. package/dist/esm/zero/graph.mjs +170 -0
  93. package/dist/esm/zero/graph.mjs.map +1 -0
  94. package/dist/esm/zero/identity.mjs +36 -0
  95. package/dist/esm/zero/identity.mjs.map +1 -0
  96. package/dist/esm/zero/index.mjs +19 -0
  97. package/dist/esm/zero/islands.mjs +200 -0
  98. package/dist/esm/zero/islands.mjs.map +1 -0
  99. package/dist/esm/zero/options.mjs +129 -0
  100. package/dist/esm/zero/options.mjs.map +1 -0
  101. package/dist/esm/zero/ownership.mjs +48 -0
  102. package/dist/esm/zero/ownership.mjs.map +1 -0
  103. package/dist/esm/zero/theme.mjs +313 -0
  104. package/dist/esm/zero/theme.mjs.map +1 -0
  105. package/dist/esm/zero/transform.mjs +182 -0
  106. package/dist/esm/zero/transform.mjs.map +1 -0
  107. package/package.json +38 -47
  108. package/src/check-dep-versions.ts +2 -2
  109. package/src/checkDeps.ts +3 -2
  110. package/src/compiler.ts +506 -0
  111. package/src/compilerHost.ts +3752 -0
  112. package/src/componentDiscovery.ts +105 -0
  113. package/src/constants.ts +0 -5
  114. package/src/domStructuralPass.ts +190 -0
  115. package/src/exports.ts +5 -6
  116. package/src/extractor/addLocalExports.ts +49 -0
  117. package/src/extractor/bundle.ts +38 -40
  118. package/src/extractor/bundleConfig.ts +368 -187
  119. package/src/extractor/concatClassName.ts +21 -24
  120. package/src/extractor/getTamaguiConfigPathFromOptionsConfig.ts +13 -5
  121. package/src/extractor/loadTamagui.ts +196 -143
  122. package/src/extractor/regenerateConfig.ts +65 -13
  123. package/src/extractor/watchTamaguiConfig.ts +6 -1
  124. package/src/helpers/requireTamaguiCore.ts +6 -1
  125. package/src/registerRequire.ts +94 -132
  126. package/src/staticEvaluationIgnoredModules.ts +74 -0
  127. package/src/types.ts +2 -107
  128. package/src/zero/artifact.ts +142 -0
  129. package/src/zero/graph.ts +359 -0
  130. package/src/zero/identity.ts +73 -0
  131. package/src/zero/index.ts +9 -0
  132. package/src/zero/islands.ts +265 -0
  133. package/src/zero/options.ts +239 -0
  134. package/src/zero/ownership.ts +113 -0
  135. package/src/zero/theme.ts +526 -0
  136. package/src/zero/transform.ts +329 -0
  137. package/types/check-dep-versions.d.ts.map +1 -1
  138. package/types/checkDeps.d.ts.map +1 -1
  139. package/types/compiler.d.ts +134 -0
  140. package/types/compiler.d.ts.map +1 -0
  141. package/types/compilerHost.d.ts +38 -0
  142. package/types/compilerHost.d.ts.map +1 -0
  143. package/types/componentDiscovery.d.ts +27 -0
  144. package/types/componentDiscovery.d.ts.map +1 -0
  145. package/types/constants.d.ts +0 -1
  146. package/types/constants.d.ts.map +1 -1
  147. package/types/domStructuralPass.d.ts +3 -0
  148. package/types/domStructuralPass.d.ts.map +1 -0
  149. package/types/exports.d.ts +5 -6
  150. package/types/exports.d.ts.map +1 -1
  151. package/types/extractor/addLocalExports.d.ts +3 -0
  152. package/types/extractor/addLocalExports.d.ts.map +1 -0
  153. package/types/extractor/bundle.d.ts +66 -65
  154. package/types/extractor/bundle.d.ts.map +1 -1
  155. package/types/extractor/bundleConfig.d.ts +26 -4
  156. package/types/extractor/bundleConfig.d.ts.map +1 -1
  157. package/types/extractor/esbuildAliasPlugin.d.ts.map +1 -1
  158. package/types/extractor/getTamaguiConfigPathFromOptionsConfig.d.ts +1 -1
  159. package/types/extractor/getTamaguiConfigPathFromOptionsConfig.d.ts.map +1 -1
  160. package/types/extractor/loadTamagui.d.ts +26 -3
  161. package/types/extractor/loadTamagui.d.ts.map +1 -1
  162. package/types/extractor/regenerateConfig.d.ts +1 -1
  163. package/types/extractor/regenerateConfig.d.ts.map +1 -1
  164. package/types/extractor/watchTamaguiConfig.d.ts.map +1 -1
  165. package/types/getPragmaOptions.d.ts +1 -1
  166. package/types/helpers/memoize.d.ts +1 -1
  167. package/types/helpers/memoize.d.ts.map +1 -1
  168. package/types/helpers/requireTamaguiCore.d.ts.map +1 -1
  169. package/types/index.d.ts.map +1 -1
  170. package/types/registerRequire.d.ts +2 -2
  171. package/types/registerRequire.d.ts.map +1 -1
  172. package/types/staticEvaluationIgnoredModules.d.ts +9 -0
  173. package/types/staticEvaluationIgnoredModules.d.ts.map +1 -0
  174. package/types/types.d.ts +2 -90
  175. package/types/types.d.ts.map +1 -1
  176. package/types/zero/artifact.d.ts +44 -0
  177. package/types/zero/artifact.d.ts.map +1 -0
  178. package/types/zero/graph.d.ts +139 -0
  179. package/types/zero/graph.d.ts.map +1 -0
  180. package/types/zero/identity.d.ts +33 -0
  181. package/types/zero/identity.d.ts.map +1 -0
  182. package/types/zero/index.d.ts +10 -0
  183. package/types/zero/index.d.ts.map +1 -0
  184. package/types/zero/islands.d.ts +56 -0
  185. package/types/zero/islands.d.ts.map +1 -0
  186. package/types/zero/options.d.ts +58 -0
  187. package/types/zero/options.d.ts.map +1 -0
  188. package/types/zero/ownership.d.ts +51 -0
  189. package/types/zero/ownership.d.ts.map +1 -0
  190. package/types/zero/theme.d.ts +85 -0
  191. package/types/zero/theme.d.ts.map +1 -0
  192. package/types/zero/transform.d.ts +53 -0
  193. package/types/zero/transform.d.ts.map +1 -0
  194. package/dist/check-dep-versions.cjs +0 -372
  195. package/dist/checkDeps.cjs +0 -271
  196. package/dist/constants.cjs +0 -51
  197. package/dist/exports.cjs +0 -48
  198. package/dist/extractor/accessSafe.cjs +0 -48
  199. package/dist/extractor/babelParse.cjs +0 -56
  200. package/dist/extractor/buildClassName.cjs +0 -86
  201. package/dist/extractor/bundle.cjs +0 -234
  202. package/dist/extractor/bundleConfig.cjs +0 -729
  203. package/dist/extractor/concatClassName.cjs +0 -129
  204. package/dist/extractor/createEvaluator.cjs +0 -88
  205. package/dist/extractor/createExtractor.cjs +0 -1968
  206. package/dist/extractor/createLogger.cjs +0 -50
  207. package/dist/extractor/detectModuleFormat.cjs +0 -55
  208. package/dist/extractor/ensureImportingConcat.cjs +0 -54
  209. package/dist/extractor/errors.cjs +0 -28
  210. package/dist/extractor/esbuildAliasPlugin.cjs +0 -56
  211. package/dist/extractor/esbuildTsconfigPaths.cjs +0 -108
  212. package/dist/extractor/evaluateAstNode.cjs +0 -128
  213. package/dist/extractor/extractHelpers.cjs +0 -182
  214. package/dist/extractor/extractMediaStyle.cjs +0 -169
  215. package/dist/extractor/extractToClassNames.cjs +0 -475
  216. package/dist/extractor/extractToNative.cjs +0 -369
  217. package/dist/extractor/findTopmostFunction.cjs +0 -43
  218. package/dist/extractor/generatedUid.cjs +0 -58
  219. package/dist/extractor/getPrefixLogs.cjs +0 -31
  220. package/dist/extractor/getPropValueFromAttributes.cjs +0 -85
  221. package/dist/extractor/getSourceModule.cjs +0 -100
  222. package/dist/extractor/getStaticBindingsForScope.cjs +0 -213
  223. package/dist/extractor/getTamaguiConfigPathFromOptionsConfig.cjs +0 -41
  224. package/dist/extractor/hasTopLevelAwait.cjs +0 -62
  225. package/dist/extractor/hoistClassNames.cjs +0 -76
  226. package/dist/extractor/literalToAst.cjs +0 -109
  227. package/dist/extractor/loadFile.cjs +0 -17
  228. package/dist/extractor/loadTamagui.cjs +0 -373
  229. package/dist/extractor/logLines.cjs +0 -43
  230. package/dist/extractor/normalizeTernaries.cjs +0 -100
  231. package/dist/extractor/propsToFontFamilyCache.cjs +0 -42
  232. package/dist/extractor/regenerateConfig.cjs +0 -178
  233. package/dist/extractor/removeUnusedHooks.cjs +0 -108
  234. package/dist/extractor/timer.cjs +0 -52
  235. package/dist/extractor/validHTMLAttributes.cjs +0 -79
  236. package/dist/extractor/watchTamaguiConfig.cjs +0 -70
  237. package/dist/getPragmaOptions.cjs +0 -73
  238. package/dist/helpers/memoize.cjs +0 -45
  239. package/dist/helpers/requireTamaguiCore.cjs +0 -40
  240. package/dist/index.cjs +0 -42
  241. package/dist/registerRequire.cjs +0 -209
  242. package/dist/server.cjs +0 -82
  243. package/dist/types.cjs +0 -18
  244. package/dist/worker.cjs +0 -111
  245. package/src/extractor/accessSafe.ts +0 -22
  246. package/src/extractor/babelParse.ts +0 -38
  247. package/src/extractor/buildClassName.ts +0 -76
  248. package/src/extractor/createEvaluator.ts +0 -82
  249. package/src/extractor/createExtractor.ts +0 -2970
  250. package/src/extractor/createLogger.ts +0 -41
  251. package/src/extractor/ensureImportingConcat.ts +0 -36
  252. package/src/extractor/errors.ts +0 -1
  253. package/src/extractor/evaluateAstNode.ts +0 -136
  254. package/src/extractor/extractHelpers.ts +0 -211
  255. package/src/extractor/extractMediaStyle.ts +0 -194
  256. package/src/extractor/extractToClassNames.ts +0 -733
  257. package/src/extractor/extractToNative.ts +0 -506
  258. package/src/extractor/findTopmostFunction.ts +0 -24
  259. package/src/extractor/generatedUid.ts +0 -41
  260. package/src/extractor/getPrefixLogs.ts +0 -9
  261. package/src/extractor/getPropValueFromAttributes.ts +0 -95
  262. package/src/extractor/getSourceModule.ts +0 -96
  263. package/src/extractor/getStaticBindingsForScope.ts +0 -237
  264. package/src/extractor/hasTopLevelAwait.ts +0 -28
  265. package/src/extractor/hoistClassNames.ts +0 -52
  266. package/src/extractor/literalToAst.ts +0 -85
  267. package/src/extractor/loadFile.ts +0 -17
  268. package/src/extractor/logLines.ts +0 -16
  269. package/src/extractor/normalizeTernaries.ts +0 -89
  270. package/src/extractor/propsToFontFamilyCache.ts +0 -18
  271. package/src/extractor/removeUnusedHooks.ts +0 -83
  272. package/src/extractor/timer.ts +0 -25
  273. package/src/extractor/validHTMLAttributes.ts +0 -52
  274. package/src/worker.ts +0 -142
  275. package/types/extractor/accessSafe.d.ts +0 -3
  276. package/types/extractor/accessSafe.d.ts.map +0 -1
  277. package/types/extractor/babelParse.d.ts +0 -5
  278. package/types/extractor/babelParse.d.ts.map +0 -1
  279. package/types/extractor/buildClassName.d.ts +0 -7
  280. package/types/extractor/buildClassName.d.ts.map +0 -1
  281. package/types/extractor/createEvaluator.d.ts +0 -12
  282. package/types/extractor/createEvaluator.d.ts.map +0 -1
  283. package/types/extractor/createExtractor.d.ts +0 -32
  284. package/types/extractor/createExtractor.d.ts.map +0 -1
  285. package/types/extractor/createLogger.d.ts +0 -3
  286. package/types/extractor/createLogger.d.ts.map +0 -1
  287. package/types/extractor/ensureImportingConcat.d.ts +0 -4
  288. package/types/extractor/ensureImportingConcat.d.ts.map +0 -1
  289. package/types/extractor/errors.d.ts +0 -3
  290. package/types/extractor/errors.d.ts.map +0 -1
  291. package/types/extractor/evaluateAstNode.d.ts +0 -3
  292. package/types/extractor/evaluateAstNode.d.ts.map +0 -1
  293. package/types/extractor/extractHelpers.d.ts +0 -28
  294. package/types/extractor/extractHelpers.d.ts.map +0 -1
  295. package/types/extractor/extractMediaStyle.d.ts +0 -11
  296. package/types/extractor/extractMediaStyle.d.ts.map +0 -1
  297. package/types/extractor/extractToClassNames.d.ts +0 -25
  298. package/types/extractor/extractToClassNames.d.ts.map +0 -1
  299. package/types/extractor/extractToNative.d.ts +0 -13
  300. package/types/extractor/extractToNative.d.ts.map +0 -1
  301. package/types/extractor/findTopmostFunction.d.ts +0 -4
  302. package/types/extractor/findTopmostFunction.d.ts.map +0 -1
  303. package/types/extractor/generatedUid.d.ts +0 -5
  304. package/types/extractor/generatedUid.d.ts.map +0 -1
  305. package/types/extractor/getPrefixLogs.d.ts +0 -3
  306. package/types/extractor/getPrefixLogs.d.ts.map +0 -1
  307. package/types/extractor/getPropValueFromAttributes.d.ts +0 -19
  308. package/types/extractor/getPropValueFromAttributes.d.ts.map +0 -1
  309. package/types/extractor/getSourceModule.d.ts +0 -16
  310. package/types/extractor/getSourceModule.d.ts.map +0 -1
  311. package/types/extractor/getStaticBindingsForScope.d.ts +0 -5
  312. package/types/extractor/getStaticBindingsForScope.d.ts.map +0 -1
  313. package/types/extractor/hasTopLevelAwait.d.ts +0 -2
  314. package/types/extractor/hasTopLevelAwait.d.ts.map +0 -1
  315. package/types/extractor/hoistClassNames.d.ts +0 -6
  316. package/types/extractor/hoistClassNames.d.ts.map +0 -1
  317. package/types/extractor/literalToAst.d.ts +0 -4
  318. package/types/extractor/literalToAst.d.ts.map +0 -1
  319. package/types/extractor/loadFile.d.ts +0 -1
  320. package/types/extractor/loadFile.d.ts.map +0 -1
  321. package/types/extractor/logLines.d.ts +0 -2
  322. package/types/extractor/logLines.d.ts.map +0 -1
  323. package/types/extractor/normalizeTernaries.d.ts +0 -5
  324. package/types/extractor/normalizeTernaries.d.ts.map +0 -1
  325. package/types/extractor/propsToFontFamilyCache.d.ts +0 -4
  326. package/types/extractor/propsToFontFamilyCache.d.ts.map +0 -1
  327. package/types/extractor/removeUnusedHooks.d.ts +0 -3
  328. package/types/extractor/removeUnusedHooks.d.ts.map +0 -1
  329. package/types/extractor/timer.d.ts +0 -5
  330. package/types/extractor/timer.d.ts.map +0 -1
  331. package/types/extractor/validHTMLAttributes.d.ts +0 -52
  332. package/types/extractor/validHTMLAttributes.d.ts.map +0 -1
  333. package/types/worker.d.ts +0 -46
  334. package/types/worker.d.ts.map +0 -1
  335. /package/dist/{setup.cjs → cjs/setup.cjs} +0 -0
@@ -0,0 +1,3752 @@
1
+ import type {
2
+ BranchDecisionNode,
3
+ CompilerLoweringHost,
4
+ CompilerTarget,
5
+ LoweringCandidateInput,
6
+ LoweringCandidateResult,
7
+ LoweringComponent,
8
+ MaterializedElement,
9
+ MaterializedStyledDefinition,
10
+ SourceEdit,
11
+ ZeroRule,
12
+ } from '@tamagui/compiler-core'
13
+ import {
14
+ collectLeaves,
15
+ zeroRuleMessage,
16
+ zeroThemeBoundaryMessage,
17
+ } from '@tamagui/compiler-core'
18
+ import {
19
+ StyleObjectIdentifier,
20
+ StyleObjectProperty,
21
+ StyleObjectRules,
22
+ StyleObjectValue,
23
+ stylePropsAll,
24
+ stylePropsInput,
25
+ stylePropsText,
26
+ validStyles as validStylesView,
27
+ } from '@tamagui/helpers'
28
+ import {
29
+ ATTRIBUTES,
30
+ DISPLAY_WEB_RESET,
31
+ EVENTS,
32
+ NATIVE_BACKING,
33
+ NATIVE_BLOCK_DEFAULTS,
34
+ NATIVE_ELEMENT_DEFAULTS,
35
+ NATIVE_FLEX_DEFAULTS,
36
+ NATIVE_INPUT_TYPES,
37
+ NATIVE_PRIMITIVE_MODULE,
38
+ TAGS,
39
+ TAG_WEB_DEFAULTS,
40
+ type TagName,
41
+ } from '@tamagui/dom'
42
+ import { resolveTransition, toCSSTransition } from '@tamagui/animation-helpers'
43
+ import {
44
+ createModifierRegistry,
45
+ parseTransition,
46
+ parseValue,
47
+ } from '@tamagui/style-grammar/tooling'
48
+ import { isValidStyleKey } from '@tamagui/web'
49
+ import type { AnimationDriver, StaticConfig, TamaguiInternalConfig } from '@tamagui/web'
50
+
51
+ import type { LoadedComponents } from './extractor/bundleConfig'
52
+ import { concatClassName } from './extractor/concatClassName'
53
+ import { requireTamaguiCore } from './helpers/requireTamaguiCore'
54
+
55
+ export interface CompilerComponentModule {
56
+ moduleName: string
57
+ resolvedId: string
58
+ }
59
+
60
+ /**
61
+ * The components lowering can resolve, keyed the way element provenance names
62
+ * them. The frontend owns one per compile and grows it as it discovers component
63
+ * modules outside the configured `components` list.
64
+ */
65
+ export interface CompilerComponentRegistry {
66
+ /** host-resolved module id -> the module name its components register under */
67
+ modulesById: Map<string, string>
68
+ componentsByModule: Map<string, LoadedComponents>
69
+ }
70
+
71
+ export function createComponentRegistry(
72
+ components: readonly LoadedComponents[],
73
+ componentModules: readonly CompilerComponentModule[]
74
+ ): CompilerComponentRegistry {
75
+ return {
76
+ modulesById: new Map(
77
+ componentModules.map((module) => [module.resolvedId, module.moduleName])
78
+ ),
79
+ componentsByModule: new Map(
80
+ components.map((component) => [component.moduleName, component])
81
+ ),
82
+ }
83
+ }
84
+
85
+ export interface TamaguiCompilerHostOptions {
86
+ target: CompilerTarget
87
+ tamaguiConfig: TamaguiInternalConfig
88
+ components: LoadedComponents[]
89
+ componentModules: CompilerComponentModule[]
90
+ /** shared with the frontend so discovery during `prepare` is visible to this host */
91
+ registry?: CompilerComponentRegistry
92
+ /** Keep elements with dynamic style props fully on the runtime path. */
93
+ disablePartialExtraction?: boolean
94
+ /** emit native theme-token mappings for the native style engine */
95
+ experimentalNativeFastPath?: boolean
96
+ /**
97
+ * Zero-runtime mode. The diagnostics stay the same shape; what changes is that
98
+ * a spread the compiler cannot prove style-free is rejected instead of merged,
99
+ * and the sites whose rule differs from their code's default say so.
100
+ */
101
+ zeroRuntime?: boolean
102
+ }
103
+
104
+ interface TamaguiLoweringComponent extends LoweringComponent {
105
+ staticConfig: StaticConfig
106
+ displayName?: string
107
+ partialRuntimeSafe: boolean
108
+ domTag?: TagName
109
+ }
110
+
111
+ const DOM_FRONTENDS = new Set([
112
+ 'tamagui',
113
+ 'tamagui/dom',
114
+ '@tamagui/core',
115
+ '@tamagui/core/dom',
116
+ '@tamagui/tailwind',
117
+ ])
118
+
119
+ const componentState = {
120
+ focus: false,
121
+ focusVisible: false,
122
+ focusWithin: false,
123
+ hover: false,
124
+ unmounted: true,
125
+ press: false,
126
+ pressIn: false,
127
+ disabled: false,
128
+ } as const
129
+
130
+ function staticObject(value: unknown): value is Record<string, unknown> {
131
+ return !!value && typeof value === 'object' && !Array.isArray(value)
132
+ }
133
+
134
+ const flatClausePattern = /(?:^|\s)@?[A-Za-z][A-Za-z0-9-]*(?:\/[A-Za-z0-9_-]+)?:/
135
+ const nativeInheritedKeywordPattern = /(?:^|:)(?:inherit|unset)(?=\s|$)/
136
+ const nativeInitialKeywordPattern = /(?:^|:)initial(?=\s|$)/
137
+ const nativeRuntimeOnlyStyleProperties = new Set(['textIndent'])
138
+
139
+ function componentKey(resolvedId: string, exportName: string): string {
140
+ return `${resolvedId}#${exportName}`
141
+ }
142
+
143
+ function cssFromRules(rules: Record<string, any>): string[] {
144
+ return Object.values(rules).flatMap((styleObject: any) => {
145
+ const identifier = styleObject?.[StyleObjectIdentifier]
146
+ const styleRules = styleObject?.[StyleObjectRules]
147
+ return identifier && Array.isArray(styleRules) ? styleRules : []
148
+ })
149
+ }
150
+
151
+ function jsxClassName(value: string): string {
152
+ return `className=${JSON.stringify(value)}`
153
+ }
154
+
155
+ function objectClassName(value: string): string {
156
+ return `className: ${JSON.stringify(value)}`
157
+ }
158
+
159
+ function serializedStyle(
160
+ style: Record<string, unknown> | null,
161
+ dynamicProperties: readonly string[]
162
+ ): string {
163
+ if (dynamicProperties.length === 0) return style ? JSON.stringify(style) : ''
164
+ return `{ ${[
165
+ ...(style
166
+ ? Object.entries(style).map(
167
+ ([name, value]) => `${JSON.stringify(name)}: ${JSON.stringify(value)}`
168
+ )
169
+ : []),
170
+ ...dynamicProperties,
171
+ ].join(', ')} }`
172
+ }
173
+
174
+ function jsxStyleAttributes(
175
+ className: string,
176
+ style: Record<string, unknown> | null,
177
+ dynamicProperties: readonly string[] = []
178
+ ) {
179
+ const serialized = serializedStyle(style, dynamicProperties)
180
+ return [
181
+ className ? jsxClassName(className) : '',
182
+ serialized ? `style={${serialized}}` : '',
183
+ ]
184
+ .filter(Boolean)
185
+ .join(' ')
186
+ }
187
+
188
+ function objectStyleProperties(
189
+ className: string,
190
+ style: Record<string, unknown> | null,
191
+ dynamicProperties: readonly string[] = []
192
+ ) {
193
+ const serialized = serializedStyle(style, dynamicProperties)
194
+ return [
195
+ className ? objectClassName(className) : '',
196
+ serialized ? `style: ${serialized}` : '',
197
+ ]
198
+ .filter(Boolean)
199
+ .join(', ')
200
+ }
201
+
202
+ function extractedStyleArtifacts(
203
+ split: any,
204
+ props: Record<string, unknown>,
205
+ config: TamaguiInternalConfig,
206
+ includeRuntimeBase = true,
207
+ hasStyleFrontend = false
208
+ ): { className: string; css: string[] } {
209
+ const callerClassName =
210
+ !hasStyleFrontend && typeof props.className === 'string' ? props.className : ''
211
+ const viewClassName =
212
+ typeof split.viewProps?.className === 'string' ? split.viewProps.className : ''
213
+ const classNameWithoutCaller =
214
+ callerClassName && viewClassName.endsWith(callerClassName)
215
+ ? viewClassName.slice(0, -callerClassName.length).trimEnd()
216
+ : viewClassName
217
+ const rules = split.rulesToInsert ?? {}
218
+ const mediaNames = new Set(Object.keys(config.media ?? {}))
219
+ const pseudoNames = [
220
+ 'hover',
221
+ 'press',
222
+ 'focus',
223
+ 'focusVisible',
224
+ 'focusWithin',
225
+ 'disabled',
226
+ ]
227
+ const buckets = {
228
+ group: [] as string[],
229
+ normal: [] as string[],
230
+ pseudo: [] as string[],
231
+ theme: [] as string[],
232
+ media: [] as string[],
233
+ }
234
+ const classKeys = new Map<string, string>()
235
+ for (const [key, identifier] of Object.entries(split.classNames ?? {})) {
236
+ if (typeof identifier === 'string') classKeys.set(identifier, key)
237
+ }
238
+ const identifiers = new Set<string>([
239
+ ...classKeys.keys(),
240
+ ...Object.values(rules).flatMap((styleObject: any) => {
241
+ const identifier = styleObject?.[StyleObjectIdentifier]
242
+ return typeof identifier === 'string' ? [identifier] : []
243
+ }),
244
+ ])
245
+ for (const identifier of identifiers) {
246
+ const key = classKeys.get(identifier) ?? ''
247
+ const css = cssFromRules(
248
+ Object.fromEntries(
249
+ Object.entries(rules).filter(
250
+ ([, styleObject]: [string, any]) =>
251
+ styleObject?.[StyleObjectIdentifier] === identifier
252
+ )
253
+ )
254
+ ).join('')
255
+ if (identifier.startsWith('t_group_')) {
256
+ buckets.group.push(identifier)
257
+ } else if (pseudoNames.some((name) => key.endsWith(`-${name}`))) {
258
+ buckets.pseudo.push(identifier)
259
+ } else if (css.includes('.t_')) {
260
+ buckets.theme.push(identifier)
261
+ } else if ([...mediaNames].some((name) => key.endsWith(`-${name}`))) {
262
+ buckets.media.push(identifier)
263
+ } else {
264
+ buckets.normal.push(identifier)
265
+ }
266
+ }
267
+ const orderedIdentifiers = [
268
+ ...buckets.group,
269
+ ...buckets.normal,
270
+ ...buckets.pseudo,
271
+ ...buckets.theme,
272
+ ...buckets.media,
273
+ ]
274
+ const orderedSet = new Set(orderedIdentifiers)
275
+ const baseViewClassName = includeRuntimeBase
276
+ ? classNameWithoutCaller
277
+ .split(/\s+/)
278
+ .filter((token) => token && !orderedSet.has(token))
279
+ .join(' ')
280
+ : ''
281
+ const css = orderedIdentifiers.flatMap((identifier) => {
282
+ const identifierRules = cssFromRules(
283
+ Object.fromEntries(
284
+ Object.entries(rules).filter(
285
+ ([, styleObject]: [string, any]) =>
286
+ styleObject?.[StyleObjectIdentifier] === identifier
287
+ )
288
+ )
289
+ )
290
+ // The legacy static theme-block path adds one root specificity level after
291
+ // resolving tokens. Keep that output contract until the legacy oracle is removed.
292
+ return buckets.theme.includes(identifier)
293
+ ? identifierRules.map((rule) =>
294
+ rule.replace(':root:root:root.t_', ':root:root:root:root.t_')
295
+ )
296
+ : identifierRules
297
+ })
298
+ return {
299
+ className: concatClassName(baseViewClassName, orderedIdentifiers, callerClassName),
300
+ css,
301
+ }
302
+ }
303
+
304
+ function spreadNonStyleReplacement(
305
+ form: MaterializedElement['form'],
306
+ entry: MaterializedElement['entries'][number],
307
+ isPropIgnored: (name: string) => boolean,
308
+ rewriteProp: (name: string, value: unknown) => [string, unknown]
309
+ ): string {
310
+ if (
311
+ entry.kind !== 'spread' ||
312
+ entry.value.kind !== 'static' ||
313
+ !staticObject(entry.value.value)
314
+ ) {
315
+ return ''
316
+ }
317
+ const nonStyleEntries = Object.entries(entry.value.value)
318
+ .filter(([key]) => !isPropIgnored(key))
319
+ .map(([key, value]) => rewriteProp(key, value))
320
+ if (nonStyleEntries.length === 0) return ''
321
+ const objectSource = `{ ${nonStyleEntries
322
+ .map(([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value)}`)
323
+ .join(', ')} }`
324
+ return form === 'jsx' ? `{...${objectSource}}` : `...${objectSource}`
325
+ }
326
+
327
+ /**
328
+ * Granular edits inside a compiled-call props object. Whole-span replacement would
329
+ * also cover the children property, so nested candidates could never both commit.
330
+ */
331
+ function compiledPropsEdits(
332
+ input: LoweringCandidateInput,
333
+ styleEntries: MaterializedElement['entries'],
334
+ replacement: string,
335
+ spreadReplacement?: (entry: MaterializedElement['entries'][number]) => string
336
+ ): SourceEdit[] | null {
337
+ const propsSpan = input.element.propsSpan
338
+ if (!propsSpan) return null
339
+ const original = input.source.slice(propsSpan.start, propsSpan.end)
340
+ const trimmed = original.trim()
341
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
342
+ return [
343
+ {
344
+ start: propsSpan.start,
345
+ end: propsSpan.end,
346
+ content: `{ ${replacement} }`,
347
+ origin: propsSpan,
348
+ },
349
+ ]
350
+ }
351
+
352
+ if (styleEntries.length === 0) {
353
+ const close = original.lastIndexOf('}')
354
+ const separator = original.slice(1, close).trim() ? ', ' : ' '
355
+ const position = propsSpan.start + close
356
+ return [
357
+ {
358
+ start: position,
359
+ end: position,
360
+ content: `${separator}${replacement} `,
361
+ origin: propsSpan,
362
+ },
363
+ ]
364
+ }
365
+
366
+ const edits: SourceEdit[] = []
367
+ for (const [index, entry] of styleEntries.entries()) {
368
+ let start = entry.span.start
369
+ let end = entry.span.end
370
+ const nonStyle = spreadReplacement?.(entry) ?? ''
371
+ if (index === 0) {
372
+ const content = [replacement, nonStyle].filter(Boolean).join(', ')
373
+ edits.push({ start, end, content, origin: entry.span })
374
+ continue
375
+ }
376
+
377
+ let cursor = end - propsSpan.start
378
+ while (cursor < original.length - 1 && /\s/.test(original[cursor]!)) cursor++
379
+ if (original[cursor] === ',') {
380
+ end = propsSpan.start + cursor + 1
381
+ } else {
382
+ cursor = start - propsSpan.start - 1
383
+ while (cursor > 0 && /\s/.test(original[cursor]!)) cursor--
384
+ if (original[cursor] === ',') start = propsSpan.start + cursor
385
+ }
386
+ edits.push({ start, end, content: nonStyle, origin: entry.span })
387
+ }
388
+
389
+ const merged: SourceEdit[] = []
390
+ for (const edit of edits.sort((left, right) => left.start - right.start)) {
391
+ const previous = merged.at(-1)
392
+ if (previous && edit.start <= previous.end && !previous.content && !edit.content) {
393
+ previous.end = Math.max(previous.end, edit.end)
394
+ } else {
395
+ merged.push(edit)
396
+ }
397
+ }
398
+ return merged
399
+ }
400
+
401
+ const compilerStyleProps = new Set([
402
+ 'className',
403
+ 'style',
404
+ 'group',
405
+ 'transition',
406
+ 'animation',
407
+ 'animatePresence',
408
+ 'animatedBy',
409
+ 'fontFamily',
410
+ 'render',
411
+ ])
412
+
413
+ const runtimeAnimationProps = new Set([
414
+ 'transition',
415
+ 'animation',
416
+ 'animatePresence',
417
+ 'animatedBy',
418
+ ])
419
+
420
+ // createComponent reads these and never forwards them to the host element, so a
421
+ // flattened element has to drop them instead of emitting an unknown attribute.
422
+ // only the inert values reach the emit: an active one already bailed.
423
+ const componentOnlyProps = new Set(['asChild', 'disableOptimization', 'themeInverse'])
424
+
425
+ const cssShorthandConflicts: Record<string, readonly string[]> = {
426
+ background: [
427
+ 'backgroundAttachment',
428
+ 'backgroundBlendMode',
429
+ 'backgroundClip',
430
+ 'backgroundColor',
431
+ 'backgroundImage',
432
+ 'backgroundOrigin',
433
+ 'backgroundPosition',
434
+ 'backgroundRepeat',
435
+ 'backgroundSize',
436
+ ],
437
+ border: [
438
+ 'borderTop',
439
+ 'borderTopColor',
440
+ 'borderTopStyle',
441
+ 'borderTopWidth',
442
+ 'borderRight',
443
+ 'borderRightColor',
444
+ 'borderRightStyle',
445
+ 'borderRightWidth',
446
+ 'borderBottom',
447
+ 'borderBottomColor',
448
+ 'borderBottomStyle',
449
+ 'borderBottomWidth',
450
+ 'borderLeft',
451
+ 'borderLeftColor',
452
+ 'borderLeftStyle',
453
+ 'borderLeftWidth',
454
+ 'borderColor',
455
+ 'borderStyle',
456
+ 'borderWidth',
457
+ 'borderImage',
458
+ 'borderImageOutset',
459
+ 'borderImageRepeat',
460
+ 'borderImageSlice',
461
+ 'borderImageSource',
462
+ 'borderImageWidth',
463
+ 'borderInlineColor',
464
+ 'borderInlineEndColor',
465
+ 'borderInlineEndStyle',
466
+ 'borderInlineEndWidth',
467
+ 'borderInlineStartColor',
468
+ 'borderInlineStartStyle',
469
+ 'borderInlineStartWidth',
470
+ 'borderInlineStyle',
471
+ 'borderInlineWidth',
472
+ ],
473
+ borderTop: ['borderTopColor', 'borderTopStyle', 'borderTopWidth'],
474
+ borderRight: [
475
+ 'borderRightColor',
476
+ 'borderRightStyle',
477
+ 'borderRightWidth',
478
+ 'borderInlineColor',
479
+ 'borderInlineEndColor',
480
+ 'borderInlineEndStyle',
481
+ 'borderInlineEndWidth',
482
+ 'borderInlineStartColor',
483
+ 'borderInlineStartStyle',
484
+ 'borderInlineStartWidth',
485
+ 'borderInlineStyle',
486
+ 'borderInlineWidth',
487
+ ],
488
+ borderBottom: ['borderBottomColor', 'borderBottomStyle', 'borderBottomWidth'],
489
+ borderLeft: [
490
+ 'borderLeftColor',
491
+ 'borderLeftStyle',
492
+ 'borderLeftWidth',
493
+ 'borderInlineColor',
494
+ 'borderInlineEndColor',
495
+ 'borderInlineEndStyle',
496
+ 'borderInlineEndWidth',
497
+ 'borderInlineStartColor',
498
+ 'borderInlineStartStyle',
499
+ 'borderInlineStartWidth',
500
+ 'borderInlineStyle',
501
+ 'borderInlineWidth',
502
+ ],
503
+ borderImage: [
504
+ 'borderImageOutset',
505
+ 'borderImageRepeat',
506
+ 'borderImageSlice',
507
+ 'borderImageSource',
508
+ 'borderImageWidth',
509
+ ],
510
+ outline: ['outlineColor', 'outlineStyle', 'outlineWidth'],
511
+ gap: ['columnGap', 'rowGap'],
512
+ gridColumn: ['gridColumnEnd', 'gridColumnStart'],
513
+ gridRow: ['gridRowEnd', 'gridRowStart'],
514
+ marginInline: [
515
+ 'marginInlineEnd',
516
+ 'marginInlineStart',
517
+ 'marginEnd',
518
+ 'marginStart',
519
+ 'marginLeft',
520
+ 'marginRight',
521
+ ],
522
+ paddingInline: [
523
+ 'paddingInlineEnd',
524
+ 'paddingInlineStart',
525
+ 'paddingEnd',
526
+ 'paddingStart',
527
+ 'paddingLeft',
528
+ 'paddingRight',
529
+ ],
530
+ insetInline: ['insetInlineEnd', 'insetInlineStart', 'end', 'start', 'left', 'right'],
531
+ borderInlineColor: [
532
+ 'borderInlineEndColor',
533
+ 'borderInlineStartColor',
534
+ 'borderEndColor',
535
+ 'borderStartColor',
536
+ 'borderLeftColor',
537
+ 'borderRightColor',
538
+ ],
539
+ borderInlineStyle: [
540
+ 'borderInlineEndStyle',
541
+ 'borderInlineStartStyle',
542
+ 'borderEndStyle',
543
+ 'borderStartStyle',
544
+ 'borderLeftStyle',
545
+ 'borderRightStyle',
546
+ ],
547
+ borderInlineWidth: [
548
+ 'borderInlineEndWidth',
549
+ 'borderInlineStartWidth',
550
+ 'borderEndWidth',
551
+ 'borderStartWidth',
552
+ 'borderLeftWidth',
553
+ 'borderRightWidth',
554
+ ],
555
+ mask: [
556
+ 'maskBorder',
557
+ 'maskBorderMode',
558
+ 'maskBorderOutset',
559
+ 'maskBorderRepeat',
560
+ 'maskBorderSlice',
561
+ 'maskBorderSource',
562
+ 'maskBorderWidth',
563
+ 'maskClip',
564
+ 'maskComposite',
565
+ 'maskImage',
566
+ 'maskMode',
567
+ 'maskOrigin',
568
+ 'maskPosition',
569
+ 'maskRepeat',
570
+ 'maskSize',
571
+ ],
572
+ maskBorder: [
573
+ 'maskBorderMode',
574
+ 'maskBorderOutset',
575
+ 'maskBorderRepeat',
576
+ 'maskBorderSlice',
577
+ 'maskBorderSource',
578
+ 'maskBorderWidth',
579
+ ],
580
+ }
581
+
582
+ const cssConflictFamilies = [
583
+ new Set(['width', 'inlineSize']),
584
+ new Set(['minWidth', 'minInlineSize']),
585
+ new Set(['maxWidth', 'maxInlineSize']),
586
+ new Set(['height', 'blockSize']),
587
+ new Set(['minHeight', 'minBlockSize']),
588
+ new Set(['maxHeight', 'maxBlockSize']),
589
+ new Set([
590
+ 'marginInline',
591
+ 'marginInlineEnd',
592
+ 'marginInlineStart',
593
+ 'marginEnd',
594
+ 'marginStart',
595
+ 'marginLeft',
596
+ 'marginRight',
597
+ ]),
598
+ new Set([
599
+ 'paddingInline',
600
+ 'paddingInlineEnd',
601
+ 'paddingInlineStart',
602
+ 'paddingEnd',
603
+ 'paddingStart',
604
+ 'paddingLeft',
605
+ 'paddingRight',
606
+ ]),
607
+ new Set([
608
+ 'insetInline',
609
+ 'insetInlineEnd',
610
+ 'insetInlineStart',
611
+ 'end',
612
+ 'start',
613
+ 'left',
614
+ 'right',
615
+ ]),
616
+ new Set([
617
+ 'borderInlineColor',
618
+ 'borderInlineEndColor',
619
+ 'borderInlineStartColor',
620
+ 'borderEndColor',
621
+ 'borderStartColor',
622
+ 'borderLeftColor',
623
+ 'borderRightColor',
624
+ ]),
625
+ new Set([
626
+ 'borderInlineStyle',
627
+ 'borderInlineEndStyle',
628
+ 'borderInlineStartStyle',
629
+ 'borderEndStyle',
630
+ 'borderStartStyle',
631
+ 'borderLeftStyle',
632
+ 'borderRightStyle',
633
+ ]),
634
+ new Set([
635
+ 'borderInlineWidth',
636
+ 'borderInlineEndWidth',
637
+ 'borderInlineStartWidth',
638
+ 'borderEndWidth',
639
+ 'borderStartWidth',
640
+ 'borderLeftWidth',
641
+ 'borderRightWidth',
642
+ ]),
643
+ ]
644
+
645
+ function cssOwnersConflict(left: Set<string>, right: Set<string>): boolean {
646
+ for (const leftOwner of left) {
647
+ for (const rightOwner of right) {
648
+ if (
649
+ leftOwner === rightOwner ||
650
+ cssShorthandConflicts[leftOwner]?.includes(rightOwner) ||
651
+ cssShorthandConflicts[rightOwner]?.includes(leftOwner) ||
652
+ cssConflictFamilies.some(
653
+ (family) => family.has(leftOwner) && family.has(rightOwner)
654
+ )
655
+ ) {
656
+ return true
657
+ }
658
+ }
659
+ }
660
+ return false
661
+ }
662
+
663
+ const runtimeEventProps = new Set([
664
+ 'onHoverIn',
665
+ 'onHoverOut',
666
+ 'onLongPress',
667
+ 'onPress',
668
+ 'onPressIn',
669
+ 'onPressOut',
670
+ ])
671
+
672
+ /**
673
+ * DOM attributes whose react native spelling is a style key, so they lower into
674
+ * the style rather than onto the host: `dir` is the text style
675
+ * `writingDirection`. An attribute already named like its style key needs no
676
+ * entry, because the split recognizes it by name. The runtime reaches the same
677
+ * place for free: its frame classifies the renamed prop against validStyles.
678
+ */
679
+ const DOM_STYLE_ATTRIBUTES = new Map(
680
+ Object.entries(ATTRIBUTES)
681
+ .filter(
682
+ ([name, row]) =>
683
+ row.native !== 'none' &&
684
+ row.nativeProp &&
685
+ row.nativeProp !== name &&
686
+ row.nativeProp in stylePropsText
687
+ )
688
+ .map(([name, row]) => [name, row.nativeProp as string])
689
+ )
690
+
691
+ // native-only: usePointerEvents maps these to touch events at runtime; a
692
+ // flattened bare RN View ignores them (RN's W3C pointer events are flag-gated
693
+ // off). on web they are real DOM events and pass through, so no bail there.
694
+ const nativePointerEventProps = new Set([
695
+ 'onPointerCancel',
696
+ 'onPointerDown',
697
+ 'onPointerEnter',
698
+ 'onPointerLeave',
699
+ 'onPointerMove',
700
+ 'onPointerUp',
701
+ ])
702
+
703
+ function isSerializableNativeStyle(value: unknown): boolean {
704
+ if (value == null || typeof value === 'number' || typeof value === 'boolean') {
705
+ return true
706
+ }
707
+ if (typeof value === 'string') return true
708
+ if (Array.isArray(value)) return value.every(isSerializableNativeStyle)
709
+ if (!staticObject(value)) return false
710
+ const prototype = Object.getPrototypeOf(value)
711
+ if (prototype !== Object.prototype && prototype !== null) return false
712
+ return Object.values(value).every(isSerializableNativeStyle)
713
+ }
714
+
715
+ function unusedIdentifier(source: string, base: string): string {
716
+ let candidate = base
717
+ let suffix = 0
718
+ while (new RegExp(`\\b${candidate}\\b`).test(source)) {
719
+ candidate = `${base}${++suffix}`
720
+ }
721
+ return candidate
722
+ }
723
+
724
+ type DOMPropEntry = Extract<MaterializedElement['entries'][number], { kind: 'prop' }>
725
+
726
+ /** props RN's <View> remaps or composes before handing them to the host component */
727
+ const VIEW_WRAPPER_PROPS =
728
+ /^(aria-|accessibilityState$|accessibilityValue$|id$|tabIndex$|nativeID$)/
729
+
730
+ /**
731
+ * Whether an element can render RN's host component directly instead of
732
+ * <View>. A child is disqualifying because <View> resets TextAncestorContext
733
+ * for its descendants when it sits inside a <Text>, and no per-element
734
+ * analysis can rule that out; with no children nothing can observe it. A
735
+ * spread is disqualifying because it can carry the aria props this checks for.
736
+ */
737
+ function isBareHostView(element: MaterializedElement): boolean {
738
+ return !element.entries.some(
739
+ (entry) =>
740
+ entry.kind === 'child' ||
741
+ entry.kind === 'spread' ||
742
+ (entry.kind === 'prop' && VIEW_WRAPPER_PROPS.test(entry.name))
743
+ )
744
+ }
745
+
746
+ function entrySource(input: LoweringCandidateInput, entry: DOMPropEntry): string {
747
+ return entry.value.kind === 'static'
748
+ ? JSON.stringify(entry.value.value)
749
+ : input.source.slice(entry.value.span.start, entry.value.span.end)
750
+ }
751
+
752
+ function renamedPropEdit(
753
+ input: LoweringCandidateInput,
754
+ entry: DOMPropEntry,
755
+ name: string
756
+ ): SourceEdit | null {
757
+ if (entry.name === name) return null
758
+ const content = input.source.slice(entry.span.start, entry.span.end)
759
+ const offset = content.indexOf(entry.name)
760
+ if (offset === -1) return null
761
+ const quoted = content[offset - 1] === '"' || content[offset - 1] === "'"
762
+ return {
763
+ start: entry.span.start + offset,
764
+ end: entry.span.start + offset + entry.name.length,
765
+ content: input.element.form === 'jsx' || quoted ? name : JSON.stringify(name),
766
+ origin: entry.span,
767
+ }
768
+ }
769
+
770
+ function serializedProps(
771
+ form: MaterializedElement['form'],
772
+ props: readonly [name: string, value: string][]
773
+ ): string {
774
+ return props
775
+ .map(([name, value]) =>
776
+ form === 'jsx' ? `${name}={${value}}` : `${JSON.stringify(name)}: ${value}`
777
+ )
778
+ .join(form === 'jsx' ? ' ' : ', ')
779
+ }
780
+
781
+ function nativeDOMProps(input: LoweringCandidateInput, tag: TagName) {
782
+ const consumed: DOMPropEntry[] = []
783
+ const edits: SourceEdit[] = []
784
+ const additions: [string, string][] = []
785
+ const nested = new Map<string, [string, string][]>()
786
+ const entries = input.element.entries.filter(
787
+ (entry): entry is DOMPropEntry => entry.kind === 'prop'
788
+ )
789
+ const names = new Set(entries.map((entry) => entry.name))
790
+ if (names.has('ref') || tag === 'br') additions.push(['__tag', JSON.stringify(tag)])
791
+ if (TAGS[tag].backing === 'text' || TAGS[tag].backing === 'textinput') {
792
+ additions.push(['__inherit', 'true'])
793
+ }
794
+ const add = (name: string, value: string) => additions.push([name, value])
795
+ const consume = (entry: DOMPropEntry) => consumed.push(entry)
796
+
797
+ for (const entry of entries) {
798
+ const value = entrySource(input, entry)
799
+ const attribute =
800
+ (entry.name in ATTRIBUTES ? ATTRIBUTES[entry.name] : undefined) ??
801
+ (entry.name.startsWith('data-') ? ATTRIBUTES['data-*'] : undefined)
802
+ const event = entry.name in EVENTS ? EVENTS[entry.name] : undefined
803
+ if (event) {
804
+ if (event.native === 'none') {
805
+ return {
806
+ consumed,
807
+ edits,
808
+ additions,
809
+ diagnostic: `${entry.name} has no native DOM event equivalent`,
810
+ diagnosticSpan: entry.span,
811
+ }
812
+ }
813
+ if (entry.name === 'onKeyDown' && tag !== 'input' && tag !== 'textarea') {
814
+ return {
815
+ consumed,
816
+ edits,
817
+ additions,
818
+ diagnostic: `onKeyDown requires a native text-entry control`,
819
+ diagnosticSpan: entry.span,
820
+ }
821
+ }
822
+ // These payloads are adapted by the hookless DOM primitives.
823
+ if (
824
+ ['onClick', 'onLoad', 'onError', 'onChange', 'onInput', 'onKeyDown'].includes(
825
+ entry.name
826
+ )
827
+ ) {
828
+ continue
829
+ }
830
+ if (event.nativeProp) {
831
+ const edit = renamedPropEdit(input, entry, event.nativeProp)
832
+ if (edit) edits.push(edit)
833
+ }
834
+ continue
835
+ }
836
+ if (!attribute || attribute.native === 'none' || entry.name === 'style') continue
837
+
838
+ if (DOM_STYLE_ATTRIBUTES.has(entry.name)) {
839
+ // the style resolution above already read it under its style key
840
+ if (entry.value.kind !== 'static') {
841
+ return {
842
+ consumed,
843
+ edits,
844
+ additions,
845
+ diagnostic: `html.${tag} ${entry.name} must be statically known for native lowering`,
846
+ diagnosticSpan: entry.span,
847
+ }
848
+ }
849
+ consume(entry)
850
+ continue
851
+ }
852
+ if (entry.name === 'hidden') {
853
+ if (entry.value.kind !== 'static') {
854
+ return {
855
+ consumed,
856
+ edits,
857
+ additions,
858
+ diagnostic: `html.${tag} hidden must be statically known for native lowering`,
859
+ diagnosticSpan: entry.span,
860
+ }
861
+ }
862
+ consume(entry)
863
+ continue
864
+ }
865
+ if (entry.name === 'tabIndex') {
866
+ consume(entry)
867
+ add('focusable', `(${value}) === 0`)
868
+ continue
869
+ }
870
+ if (entry.name === 'readOnly') {
871
+ consume(entry)
872
+ add('editable', `!(${value})`)
873
+ continue
874
+ }
875
+ if (entry.name === 'disabled') {
876
+ consume(entry)
877
+ add('disabled', value)
878
+ add('focusable', `!(${value})`)
879
+ if (tag === 'input' || tag === 'textarea') add('editable', `!(${value})`)
880
+ const state = nested.get('accessibilityState') ?? []
881
+ state.push(['disabled', value])
882
+ nested.set('accessibilityState', state)
883
+ continue
884
+ }
885
+ if (entry.name === 'type') {
886
+ consume(entry)
887
+ if (tag === 'input' && entry.value.kind !== 'static') {
888
+ return {
889
+ consumed,
890
+ edits,
891
+ additions,
892
+ diagnostic: `html.input type must be statically known for native lowering`,
893
+ diagnosticSpan: entry.span,
894
+ }
895
+ }
896
+ if (tag === 'input' && entry.value.kind === 'static') {
897
+ const type = entry.value.value
898
+ if (type === 'password') add('secureTextEntry', 'true')
899
+ else if (
900
+ typeof type === 'string' &&
901
+ type !== 'text' &&
902
+ NATIVE_INPUT_TYPES.includes(type)
903
+ ) {
904
+ if (!names.has('inputMode')) {
905
+ add('inputMode', JSON.stringify(type === 'number' ? 'numeric' : type))
906
+ }
907
+ }
908
+ }
909
+ continue
910
+ }
911
+ if (entry.name === 'aria-hidden') {
912
+ consume(entry)
913
+ add('accessibilityElementsHidden', value)
914
+ add('importantForAccessibility', `(${value}) ? "no-hide-descendants" : "auto"`)
915
+ continue
916
+ }
917
+ if (entry.name === 'aria-live') {
918
+ consume(entry)
919
+ add(
920
+ 'accessibilityLiveRegion',
921
+ entry.value.kind === 'static' && entry.value.value === 'off' ? '"none"' : value
922
+ )
923
+ continue
924
+ }
925
+ const nativeProp = attribute.nativeProp
926
+ if (!nativeProp) continue
927
+ if (nativeProp.includes('.')) {
928
+ consume(entry)
929
+ const [parent, child] = nativeProp.split('.') as [string, string]
930
+ const values = nested.get(parent) ?? []
931
+ values.push([child, value])
932
+ nested.set(parent, values)
933
+ continue
934
+ }
935
+ const edit = renamedPropEdit(input, entry, nativeProp)
936
+ if (edit) edits.push(edit)
937
+ }
938
+
939
+ for (const [name, values] of nested) {
940
+ add(name, `{ ${values.map(([key, value]) => `${key}: ${value}`).join(', ')} }`)
941
+ }
942
+ if (!names.has('role') && TAGS[tag].role) add('role', JSON.stringify(TAGS[tag].role))
943
+ if (tag === 'textarea') add('multiline', 'true')
944
+ return { consumed, edits, additions }
945
+ }
946
+
947
+ function webDOMProps(input: LoweringCandidateInput, tag: TagName) {
948
+ const edits: SourceEdit[] = []
949
+ const additions: [string, string][] = []
950
+ const entries = input.element.entries.filter(
951
+ (entry): entry is DOMPropEntry => entry.kind === 'prop'
952
+ )
953
+ const names = new Set(entries.map((entry) => entry.name))
954
+ for (const entry of entries) {
955
+ if (entry.name === 'for') {
956
+ const edit = renamedPropEdit(input, entry, 'htmlFor')
957
+ if (edit) edits.push(edit)
958
+ }
959
+ if (
960
+ entry.name === 'role' &&
961
+ entry.value.kind === 'static' &&
962
+ entry.value.value === 'none'
963
+ ) {
964
+ edits.push({
965
+ start: entry.value.span.start,
966
+ end: entry.value.span.end,
967
+ content: JSON.stringify('presentation'),
968
+ origin: entry.value.span,
969
+ })
970
+ }
971
+ }
972
+ if (tag === 'button' && !names.has('type')) additions.push(['type', '"button"'])
973
+ if ((tag === 'input' || tag === 'textarea') && !names.has('dir')) {
974
+ additions.push(['dir', '"auto"'])
975
+ }
976
+ return { edits, additions }
977
+ }
978
+
979
+ export function createTamaguiCompilerHost(
980
+ options: TamaguiCompilerHostOptions
981
+ ): CompilerLoweringHost {
982
+ const platform = options.target === 'native' ? 'native' : 'web'
983
+ const core = requireTamaguiCore(platform) as any
984
+ const compilerVariantStyleResolver = core.styled(core.View, {}).staticConfig
985
+ .variantStyleResolver
986
+ const firstThemeName = Object.keys(options.tamaguiConfig.themes ?? {})[0] ?? ''
987
+ const firstTheme = options.tamaguiConfig.themes?.[firstThemeName] ?? {}
988
+ const theme = firstTheme
989
+ const modifierRegistry = createModifierRegistry({
990
+ mediaNames: options.tamaguiConfig.media ?? {},
991
+ themeNames: options.tamaguiConfig.themes ?? {},
992
+ }).registry
993
+ // the structured twin of flatClausePattern: a flat conditional object names a
994
+ // `default` or opens with a resolvable modifier chain, matching the runtime's
995
+ // first-key discrimination; a structured leaf (shadowOffset) matches neither
996
+ const isClauseObjectValue = (value: unknown): boolean => {
997
+ if (!staticObject(value)) return false
998
+ if ('default' in value) return true
999
+ for (const key in value) {
1000
+ if (key.length === 0) return false
1001
+ let start = 0
1002
+ for (let index = 0; index <= key.length; index++) {
1003
+ if (index !== key.length && key.charCodeAt(index) !== 58) continue
1004
+ if (modifierRegistry.get(key.slice(start, index)) === undefined) return false
1005
+ start = index + 1
1006
+ }
1007
+ return true
1008
+ }
1009
+ return false
1010
+ }
1011
+ // A native bundle knows one thing about its platform: it is not web. So a
1012
+ // `web:` clause can never match there and a bare `native:` clause always
1013
+ // does, which makes a value built only from those two resolvable at compile
1014
+ // time. Every other modifier is live at render: media and container queries
1015
+ // measure, theme and group clauses read context, state and lifecycle clauses
1016
+ // track the component, and the device platforms (ios, android, tv) are not
1017
+ // known until the bundle runs. One of those anywhere in the value keeps the
1018
+ // whole element on the runtime path.
1019
+ const platformModifiersRegistered =
1020
+ modifierRegistry.get('web') === 'platform' &&
1021
+ modifierRegistry.get('native') === 'platform'
1022
+ const nativeClauseValue = (
1023
+ value: unknown
1024
+ ): 'live' | { payload: unknown; matched: boolean } | null => {
1025
+ if (typeof value !== 'string' || !flatClausePattern.test(value)) {
1026
+ return isClauseObjectValue(value) ? 'live' : null
1027
+ }
1028
+ if (!platformModifiersRegistered) return 'live'
1029
+ const parsed = parseValue(value, modifierRegistry)
1030
+ if (!parsed.ok || parsed.value.clauses.length === 0) return 'live'
1031
+ let payload: unknown = parsed.value.base
1032
+ let matched = parsed.value.base !== null
1033
+ for (const clause of parsed.value.clauses) {
1034
+ // a chain containing `web` is dead here whatever else it names
1035
+ if (clause.modifiers.includes('web')) continue
1036
+ if (clause.modifiers.length !== 1 || clause.modifiers[0] !== 'native') return 'live'
1037
+ // platform clauses all rank the same, so a later one wins and any of them
1038
+ // beats the unconditional base
1039
+ payload = clause.payload
1040
+ matched = true
1041
+ }
1042
+ return { payload, matched }
1043
+ }
1044
+ const configuredAnimationDriver = options.tamaguiConfig.animations as
1045
+ | AnimationDriver
1046
+ | undefined
1047
+ const configuredCssAnimationDriver =
1048
+ platform === 'web' &&
1049
+ configuredAnimationDriver?.outputStyle === 'css' &&
1050
+ !options.tamaguiConfig.animationDrivers
1051
+ ? configuredAnimationDriver
1052
+ : null
1053
+ const resolveStaticCssTransition = (
1054
+ value: unknown,
1055
+ transitionPresets: Record<string, unknown>
1056
+ ): string | null => {
1057
+ if (platform !== 'web' || typeof value !== 'string') return null
1058
+ const transitionPresetNames = new Set(Object.keys(transitionPresets))
1059
+ const program = parseValue(value, modifierRegistry)
1060
+ if (!program.ok) return null
1061
+ const resolvedPayloads: string[] = []
1062
+ const payloads = [
1063
+ program.value.base,
1064
+ ...program.value.clauses.map((clause) => clause.payload),
1065
+ ].filter((payload): payload is string => payload !== null)
1066
+ if (payloads.length === 0) return null
1067
+ for (const payload of payloads) {
1068
+ const transition = parseTransition(payload, transitionPresetNames)
1069
+ if (!transition.ok) return null
1070
+ if (transition.value.kind === 'global') {
1071
+ resolvedPayloads.push(payload)
1072
+ continue
1073
+ }
1074
+ // the runtime's own resolver, so a preset lowers to exactly the css the
1075
+ // css driver would have emitted. a preset of any shape works, springs
1076
+ // included: they become a `linear()` easing here rather than forcing the
1077
+ // element onto the runtime path.
1078
+ const resolved = resolveTransition(payload, { animations: transitionPresets })
1079
+ if (resolved.diagnostics.length > 0) return null
1080
+ if (!resolved.fused) {
1081
+ resolvedPayloads.push(payload)
1082
+ continue
1083
+ }
1084
+ const css = toCSSTransition(resolved)
1085
+ if (css === undefined) return null
1086
+ resolvedPayloads.push(css)
1087
+ }
1088
+ let payloadIndex = 0
1089
+ const resolved: string[] = []
1090
+ if (program.value.base !== null) {
1091
+ resolved.push(resolvedPayloads[payloadIndex++]!)
1092
+ }
1093
+ for (const clause of program.value.clauses) {
1094
+ resolved.push(`${clause.modifiers.join(':')}:${resolvedPayloads[payloadIndex++]!}`)
1095
+ }
1096
+ return resolved.join(' ')
1097
+ }
1098
+ const { modulesById, componentsByModule } =
1099
+ options.registry ??
1100
+ createComponentRegistry(options.components, options.componentModules)
1101
+
1102
+ // A component's import provenance chose its authoring syntax, and the frontend
1103
+ // descriptor frozen onto its static config is the only thing that knows how to
1104
+ // resolve that syntax's static input (a class-string base, string variants) into
1105
+ // styles. Regular Tamagui components carry no descriptor
1106
+ // because their static config is already the renderer's input shape. The compiler
1107
+ // must never import a frontend package: `@tamagui/tailwind/vite` already depends on
1108
+ // the static compiler, so reaching the other way would be a cycle.
1109
+ const normalizedStyleConfigs = new WeakMap<StaticConfig, any>()
1110
+ const normalizeStaticConfig = (staticConfig: StaticConfig): StaticConfig => {
1111
+ if (!staticConfig.styleFrontend) return staticConfig
1112
+ const normalized = core.getStyleStaticConfig(staticConfig, options.tamaguiConfig)
1113
+ const result = {
1114
+ ...staticConfig,
1115
+ baseStyle: normalized.baseStyle,
1116
+ defaultProps: normalized.defaultProps,
1117
+ variants: normalized.variants,
1118
+ passthroughClassName: normalized.passthroughClassName,
1119
+ }
1120
+ normalizedStyleConfigs.set(result, normalized)
1121
+ return result
1122
+ }
1123
+
1124
+ const directStaticConfig = (
1125
+ element: MaterializedElement
1126
+ ): { key: string; staticConfig: StaticConfig; displayName?: string } | null => {
1127
+ const identity = element.component.provenance
1128
+ if (!identity) return null
1129
+ const moduleName = modulesById.get(identity.resolvedId)
1130
+ const component = moduleName ? componentsByModule.get(moduleName) : undefined
1131
+ const info = component?.nameToInfo[identity.importedName]
1132
+ return info
1133
+ ? {
1134
+ key: componentKey(identity.resolvedId, identity.importedName),
1135
+ staticConfig: normalizeStaticConfig(info.staticConfig),
1136
+ displayName: info.displayName,
1137
+ }
1138
+ : null
1139
+ }
1140
+
1141
+ const domStaticConfig = (
1142
+ element: MaterializedElement
1143
+ ): {
1144
+ key: string
1145
+ staticConfig: StaticConfig
1146
+ displayName: string
1147
+ tag: TagName
1148
+ } | null => {
1149
+ const identity = element.component.provenance
1150
+ const tag = element.component.name as TagName
1151
+ if (
1152
+ identity?.importedName !== 'html' ||
1153
+ !DOM_FRONTENDS.has(identity.specifier) ||
1154
+ !(tag in TAGS)
1155
+ ) {
1156
+ return null
1157
+ }
1158
+ const row = TAGS[tag]
1159
+ const textLike = row.backing === 'text' || row.backing === 'textinput'
1160
+ const base = (textLike ? core.Text : core.View)?.staticConfig as
1161
+ | StaticConfig
1162
+ | undefined
1163
+ if (!base) return null
1164
+ const platformDefaults =
1165
+ platform === 'web'
1166
+ ? {
1167
+ ...DISPLAY_WEB_RESET[row.display],
1168
+ ...row.defaults,
1169
+ ...TAG_WEB_DEFAULTS[tag],
1170
+ }
1171
+ : {
1172
+ ...NATIVE_ELEMENT_DEFAULTS,
1173
+ ...(row.display === 'block' ? NATIVE_BLOCK_DEFAULTS : null),
1174
+ ...row.defaults,
1175
+ ...row.nativeDefaults,
1176
+ }
1177
+ return {
1178
+ key: componentKey(identity.resolvedId, `html.${tag}`),
1179
+ tag,
1180
+ displayName: tag,
1181
+ staticConfig: normalizeStaticConfig({
1182
+ ...base,
1183
+ isInput: row.backing === 'textinput',
1184
+ // CSS text properties may be authored on any element and inherited by
1185
+ // descendant text, including from a View-backed tag on native.
1186
+ validStyles: {
1187
+ ...validStylesView,
1188
+ ...(row.backing === 'textinput' ? stylePropsInput : stylePropsText),
1189
+ },
1190
+ defaultProps: {
1191
+ ...base.defaultProps,
1192
+ ...platformDefaults,
1193
+ },
1194
+ }),
1195
+ }
1196
+ }
1197
+
1198
+ const styledStaticConfig = (
1199
+ definition: MaterializedStyledDefinition | null
1200
+ ): { key: string; staticConfig: StaticConfig; displayName?: string } | null => {
1201
+ if (!definition || definition.options.kind !== 'static') return null
1202
+ const staticConfig = definition.staticConfig
1203
+ if (
1204
+ staticConfig &&
1205
+ (staticConfig.kind !== 'static' || !staticObject(staticConfig.value))
1206
+ ) {
1207
+ return null
1208
+ }
1209
+ const base = directStaticConfig({
1210
+ kind: 'element',
1211
+ form: 'jsx',
1212
+ id: definition.id,
1213
+ span: definition.span,
1214
+ propsSpan: null,
1215
+ component: definition.base,
1216
+ complete: true,
1217
+ entries: [],
1218
+ bailouts: [],
1219
+ })
1220
+ if (!base || !staticObject(definition.options.value)) return null
1221
+ const {
1222
+ variants,
1223
+ defaultVariants,
1224
+ displayName,
1225
+ context,
1226
+ contextProps,
1227
+ ...defaultProps
1228
+ } = definition.options.value as Record<string, any>
1229
+ const baseClassName =
1230
+ definition.baseClassName?.kind === 'static' &&
1231
+ typeof definition.baseClassName.value === 'string'
1232
+ ? definition.baseClassName.value
1233
+ : undefined
1234
+ const localStaticConfig = {
1235
+ ...base.staticConfig,
1236
+ ...(staticConfig?.value as Record<string, unknown> | undefined),
1237
+ variants: {
1238
+ ...base.staticConfig.variants,
1239
+ ...(variants as object | undefined),
1240
+ },
1241
+ defaultProps: {
1242
+ ...base.staticConfig.defaultProps,
1243
+ ...defaultProps,
1244
+ ...(defaultVariants as object | undefined),
1245
+ },
1246
+ defaultVariants,
1247
+ baseClassName: [base.staticConfig.baseClassName, baseClassName]
1248
+ .filter(Boolean)
1249
+ .join(' '),
1250
+ context: context ?? base.staticConfig.context,
1251
+ contextProps: context
1252
+ ? contextProps
1253
+ : (contextProps ?? base.staticConfig.contextProps),
1254
+ }
1255
+ ;(localStaticConfig as any).variantStyleResolver = compilerVariantStyleResolver
1256
+ return {
1257
+ key: componentKey(definition.id, definition.name),
1258
+ displayName: displayName || base.displayName,
1259
+ staticConfig: normalizeStaticConfig(localStaticConfig),
1260
+ }
1261
+ }
1262
+
1263
+ const resolve = (
1264
+ element: MaterializedElement,
1265
+ styledDefinition: MaterializedStyledDefinition | null
1266
+ ): TamaguiLoweringComponent | null => {
1267
+ const dom = domStaticConfig(element)
1268
+ // a config the host evaluated is the component's real static config. The
1269
+ // one derived from a visible `styled()` literal only stands in for
1270
+ // definitions nothing evaluated, such as the app's own
1271
+ const resolved =
1272
+ dom ?? directStaticConfig(element) ?? styledStaticConfig(styledDefinition)
1273
+ if (!resolved) return null
1274
+ const defaultProps = resolved.staticConfig.defaultProps ?? {}
1275
+ return {
1276
+ key: resolved.key,
1277
+ canFlatten:
1278
+ resolved.staticConfig.acceptsClassName !== false &&
1279
+ !resolved.staticConfig.neverFlatten &&
1280
+ !resolved.staticConfig.context,
1281
+ staticConfig: resolved.staticConfig,
1282
+ displayName: resolved.displayName,
1283
+ ...(dom && { domTag: dom.tag }),
1284
+ // retaining the component also retains these runtime style sources.
1285
+ // splitting their output into equal-specificity atomic classes would
1286
+ // make stylesheet insertion order decide the winner.
1287
+ partialRuntimeSafe:
1288
+ !dom &&
1289
+ !styledDefinition &&
1290
+ Object.keys(defaultProps).length === 0 &&
1291
+ !resolved.staticConfig.defaultVariants &&
1292
+ !resolved.staticConfig.baseClassName &&
1293
+ !resolved.staticConfig.baseStyle,
1294
+ }
1295
+ }
1296
+
1297
+ const isStyleProp = (name: string, component: LoweringComponent): boolean => {
1298
+ const staticConfig = component.staticConfig as StaticConfig
1299
+ return (
1300
+ compilerStyleProps.has(name) ||
1301
+ name in (options.tamaguiConfig.shorthands ?? {}) ||
1302
+ !!staticConfig.validStyles?.[name] ||
1303
+ !!staticConfig.variants?.[name]
1304
+ )
1305
+ }
1306
+
1307
+ // props the compiler can never pre-resolve per-branch: they carry runtime
1308
+ // semantics beyond style resolution. Everything else that isStyleProp admits
1309
+ // — including fontFamily and variants — is safe for conditional lowering,
1310
+ // because each branch runs the full resolveSplitStyles pipeline
1311
+ const runtimeOnlyStyleProps = new Set([
1312
+ 'className',
1313
+ 'style',
1314
+ 'group',
1315
+ 'transition',
1316
+ 'animation',
1317
+ 'animatePresence',
1318
+ 'animatedBy',
1319
+ 'render',
1320
+ ])
1321
+ // a frontend's className is its style input: each value it can take
1322
+ // resolves through the same pipeline as a static class string
1323
+ const isFrontendClassName = (name: string, component: LoweringComponent): boolean =>
1324
+ name === 'className' && !!(component.staticConfig as StaticConfig).styleFrontend
1325
+ const canLowerConditionalStyleProp = (
1326
+ name: string,
1327
+ component: LoweringComponent
1328
+ ): boolean =>
1329
+ isStyleProp(name, component) &&
1330
+ (!runtimeOnlyStyleProps.has(name) || isFrontendClassName(name, component))
1331
+ // the finite class strings a frontend className takes at runtime (a template
1332
+ // over a static array with a dynamic index, a static array member). each one
1333
+ // lowers like a conditional branch, selected by the runtime string
1334
+ const frontendClassDomain = (
1335
+ entry: MaterializedElement['entries'][number],
1336
+ component: LoweringComponent
1337
+ ): string[] | null => {
1338
+ if (
1339
+ entry.kind !== 'prop' ||
1340
+ entry.value.kind !== 'bailout' ||
1341
+ !isFrontendClassName(entry.name, component)
1342
+ ) {
1343
+ return null
1344
+ }
1345
+ const dynamic = entry.value.dynamic
1346
+ if (dynamic?.type !== 'string' || !dynamic.values?.length) return null
1347
+ return dynamic.values.map(String)
1348
+ }
1349
+ type DynamicStyleEntry = Extract<
1350
+ MaterializedElement['entries'][number],
1351
+ { kind: 'prop' }
1352
+ > & {
1353
+ /** a member of the element's `style` object, lowered like the direct prop of its name */
1354
+ styleMember?: true
1355
+ }
1356
+ // an entry that lowers per branch as classes on web
1357
+ const conditionalClassEntry = (
1358
+ entry: DynamicStyleEntry,
1359
+ component: LoweringComponent
1360
+ ): boolean =>
1361
+ (entry.value.kind === 'conditional' &&
1362
+ canLowerConditionalStyleProp(entry.name, component)) ||
1363
+ frontendClassDomain(entry, component) !== null
1364
+
1365
+ const isInvalidHostStyleProp = (
1366
+ name: string,
1367
+ component: LoweringComponent
1368
+ ): boolean => {
1369
+ const staticConfig = component.staticConfig as StaticConfig
1370
+ const validStyles =
1371
+ staticConfig.validStyles ||
1372
+ (staticConfig.isInput
1373
+ ? stylePropsInput
1374
+ : staticConfig.isText
1375
+ ? stylePropsText
1376
+ : validStylesView)
1377
+ return name in stylePropsAll && !isValidStyleKey(name, validStyles)
1378
+ }
1379
+
1380
+ const directStyleName = (name: string, component: LoweringComponent): string | null => {
1381
+ if (compilerStyleProps.has(name) || name === 'style') {
1382
+ return null
1383
+ }
1384
+ const staticConfig = component.staticConfig as StaticConfig
1385
+ if (staticConfig.variants?.[name]) return null
1386
+ const expanded = options.tamaguiConfig.shorthands?.[name] ?? name
1387
+ return staticConfig.validStyles?.[expanded] ? expanded : null
1388
+ }
1389
+
1390
+ const resolveSplitStyles = (
1391
+ props: Record<string, unknown>,
1392
+ staticConfig: StaticConfig,
1393
+ animationDriver?: AnimationDriver | null,
1394
+ displayName?: string
1395
+ ) => {
1396
+ const previousStatic = process.env.IS_STATIC
1397
+ const previousTarget = process.env.TAMAGUI_TARGET
1398
+ if (platform === 'native') {
1399
+ process.env.IS_STATIC = 'is_static'
1400
+ } else {
1401
+ delete process.env.IS_STATIC
1402
+ }
1403
+ process.env.TAMAGUI_TARGET = platform
1404
+ try {
1405
+ return core.getSplitStyles(
1406
+ props,
1407
+ staticConfig,
1408
+ theme,
1409
+ firstThemeName,
1410
+ componentState,
1411
+ {
1412
+ isStatic: true,
1413
+ resolveValues: platform === 'native' ? 'except-theme' : 'variable',
1414
+ noClass: platform === 'native',
1415
+ isAnimated: false,
1416
+ displayName,
1417
+ },
1418
+ undefined,
1419
+ undefined,
1420
+ undefined,
1421
+ undefined,
1422
+ undefined,
1423
+ undefined,
1424
+ animationDriver,
1425
+ normalizedStyleConfigs.get(staticConfig)
1426
+ )
1427
+ } finally {
1428
+ if (previousStatic === undefined) delete process.env.IS_STATIC
1429
+ else process.env.IS_STATIC = previousStatic
1430
+ if (previousTarget === undefined) delete process.env.TAMAGUI_TARGET
1431
+ else process.env.TAMAGUI_TARGET = previousTarget
1432
+ }
1433
+ }
1434
+
1435
+ const partialStaticConfig = (staticConfig: StaticConfig): StaticConfig => ({
1436
+ ...staticConfig,
1437
+ baseStyle: undefined,
1438
+ defaultProps: {},
1439
+ defaultVariants: undefined,
1440
+ variants: {},
1441
+ })
1442
+
1443
+ const styleOwners = (
1444
+ name: string,
1445
+ value: unknown,
1446
+ staticConfig: StaticConfig
1447
+ ): Set<string> | null => {
1448
+ const split = resolveSplitStyles({ [name]: value }, partialStaticConfig(staticConfig))
1449
+ if (!split) return null
1450
+
1451
+ const inlineStyle = split.viewProps?.style
1452
+ if (staticObject(inlineStyle) && Object.keys(inlineStyle).length > 0) {
1453
+ return null
1454
+ }
1455
+
1456
+ const owners = new Set<string>(Object.keys(split.classNames ?? {}))
1457
+ for (const styleObject of Object.values(split.rulesToInsert ?? {}) as any[]) {
1458
+ const property = styleObject?.[StyleObjectProperty]
1459
+ if (typeof property === 'string') owners.add(property)
1460
+ }
1461
+ return owners.size > 0 ? owners : null
1462
+ }
1463
+
1464
+ const dynamicStyleOwners = (
1465
+ name: string,
1466
+ staticConfig: StaticConfig
1467
+ ): Set<string> | null => {
1468
+ // most web style normalization depends only on the prop name. these values
1469
+ // exercise the few structured inputs so getSplitStyles can report the
1470
+ // normalized CSS property it would own. flex is value-dependent, so include
1471
+ // every property produced by both its numeric and CSS-shorthand forms.
1472
+ const probeValue =
1473
+ name === 'transform'
1474
+ ? [{ scale: 1 }]
1475
+ : name === 'transformMatrix'
1476
+ ? [1, 0, 0, 1, 0, 0]
1477
+ : name === 'shadowOffset'
1478
+ ? { width: 0, height: 0 }
1479
+ : name === 'shadowColor'
1480
+ ? 'black'
1481
+ : name === 'border' || name === 'outline'
1482
+ ? '0 solid transparent'
1483
+ : name === 'position'
1484
+ ? 'relative'
1485
+ : name === 'objectFit'
1486
+ ? 'contain'
1487
+ : 0
1488
+ const owners = styleOwners(name, probeValue, staticConfig)
1489
+ if (!owners || name !== 'flex') return owners
1490
+ const shorthandOwners = styleOwners(name, '0 1 auto', staticConfig)
1491
+ if (!shorthandOwners) return null
1492
+ for (const owner of shorthandOwners) owners.add(owner)
1493
+ return owners
1494
+ }
1495
+
1496
+ const developmentDebugInstrumentation = (
1497
+ input: LoweringCandidateInput,
1498
+ result: LoweringCandidateResult
1499
+ ) => {
1500
+ if (process.env.NODE_ENV !== 'development') return
1501
+ const debugEntry = input.element.entries.find(
1502
+ (entry) =>
1503
+ entry.kind === 'prop' &&
1504
+ entry.name === 'debug' &&
1505
+ entry.value.kind === 'static' &&
1506
+ entry.value.value === 'verbose'
1507
+ )
1508
+ if (!debugEntry) return
1509
+
1510
+ const component = input.component as TamaguiLoweringComponent
1511
+ const styleEntries = input.element.entries.filter(
1512
+ (entry): entry is DOMPropEntry =>
1513
+ entry.kind === 'prop' &&
1514
+ entry.name !== 'debug' &&
1515
+ (entry.name === 'style' || isStyleProp(entry.name, component))
1516
+ )
1517
+ const flattened = result.ok && !!result.flattened
1518
+ const tiers = result.ok
1519
+ ? [
1520
+ 'lowered' as const,
1521
+ ...(flattened ? (['flattened'] as const) : []),
1522
+ ...(input.styledDefinition ? (['styled'] as const) : []),
1523
+ ]
1524
+ : (['bailed'] as const)
1525
+ const why = result.ok
1526
+ ? flattened
1527
+ ? 'Static styles were lowered and the component was flattened to a host element.'
1528
+ : 'Static styles were lowered while the Tamagui component remained at runtime.'
1529
+ : result.bailout.message
1530
+
1531
+ const styles = styleEntries.map((entry) => {
1532
+ if (!result.ok) {
1533
+ return {
1534
+ prop: entry.name,
1535
+ tier: 'bailed' as const,
1536
+ runtime: true as const,
1537
+ why: `${result.bailout.message}. The Tamagui runtime resolved this prop.`,
1538
+ }
1539
+ }
1540
+
1541
+ const edited = result.edits.some(
1542
+ (edit) => edit.start < entry.span.end && edit.end > entry.span.start
1543
+ )
1544
+ if (!flattened && !edited) {
1545
+ return {
1546
+ prop: entry.name,
1547
+ tier: 'bailed' as const,
1548
+ runtime: true as const,
1549
+ why: 'This prop remained for Tamagui runtime resolution.',
1550
+ }
1551
+ }
1552
+
1553
+ if (entry.value.kind === 'static') {
1554
+ const split = resolveSplitStyles(
1555
+ { [entry.name]: entry.value.value },
1556
+ partialStaticConfig(component.staticConfig)
1557
+ )
1558
+ const hasOutput = Boolean(
1559
+ split &&
1560
+ (Object.keys(split.classNames ?? {}).length > 0 ||
1561
+ Object.keys(split.rulesToInsert ?? {}).length > 0 ||
1562
+ (staticObject(split.style) && Object.keys(split.style).length > 0) ||
1563
+ (staticObject(split.viewProps?.style) &&
1564
+ Object.keys(split.viewProps.style).length > 0))
1565
+ )
1566
+ if (!hasOutput) {
1567
+ return {
1568
+ prop: entry.name,
1569
+ tier: flattened ? ('flattened' as const) : ('lowered' as const),
1570
+ dropped: true as const,
1571
+ why: `No ${platform} style output was produced for this prop.`,
1572
+ }
1573
+ }
1574
+ }
1575
+
1576
+ return {
1577
+ prop: entry.name,
1578
+ tier: flattened ? ('flattened' as const) : ('lowered' as const),
1579
+ why: flattened
1580
+ ? 'The compiler emitted this prop on the flattened host element.'
1581
+ : 'The compiler lowered this prop while retaining the Tamagui component.',
1582
+ }
1583
+ })
1584
+
1585
+ const receipt = JSON.stringify({
1586
+ component: input.element.component.name,
1587
+ tiers,
1588
+ why,
1589
+ styles,
1590
+ })
1591
+ const content = flattened
1592
+ ? input.element.form === 'jsx'
1593
+ ? `{...(console.info('🔹 Tamagui style receipt', ${receipt}), {})}`
1594
+ : `...(console.info('🔹 Tamagui style receipt', ${receipt}), {})`
1595
+ : input.element.form === 'jsx'
1596
+ ? `debug="verbose" __tamaguiStyleDebugReceipt={${receipt}}`
1597
+ : `debug: "verbose", __tamaguiStyleDebugReceipt: ${receipt}`
1598
+
1599
+ return [
1600
+ {
1601
+ start: debugEntry.span.start,
1602
+ end: debugEntry.span.end,
1603
+ content,
1604
+ origin: debugEntry.span,
1605
+ },
1606
+ ]
1607
+ }
1608
+
1609
+ return {
1610
+ resolveComponent: resolve,
1611
+ isStyleProp,
1612
+ canLowerDynamicStyleProp(name, component, valueKind) {
1613
+ // a conditional with static branches lowers per-branch on both
1614
+ // platforms: each side resolves at compile time, only the test survives
1615
+ if (
1616
+ !options.disablePartialExtraction &&
1617
+ valueKind === 'conditional' &&
1618
+ canLowerConditionalStyleProp(name, component)
1619
+ ) {
1620
+ return true
1621
+ }
1622
+ // A component resolver receives the complete props object. Without a
1623
+ // compiler-derived read set, resolving an opaque dynamic callsite would
1624
+ // execute the real resolver with an incomplete props snapshot.
1625
+ if ((component.staticConfig as StaticConfig).resolvers?.length) {
1626
+ return false
1627
+ }
1628
+ if (
1629
+ !options.disablePartialExtraction &&
1630
+ platform === 'web' &&
1631
+ isFrontendClassName(name, component)
1632
+ ) {
1633
+ return true
1634
+ }
1635
+ return (
1636
+ !options.disablePartialExtraction &&
1637
+ ((platform === 'web' && !!directStyleName(name, component)) ||
1638
+ (platform === 'native' && directStyleName(name, component) === 'opacity'))
1639
+ )
1640
+ },
1641
+ developmentDebugInstrumentation,
1642
+ lowerCandidate(input): LoweringCandidateResult {
1643
+ const component = input.component as TamaguiLoweringComponent
1644
+ if (!component.canFlatten) {
1645
+ const reason =
1646
+ component.staticConfig.acceptsClassName === false
1647
+ ? `${component.key} does not accept className`
1648
+ : component.staticConfig.neverFlatten
1649
+ ? `${component.key} is never flattened (behavior HOC)`
1650
+ : `${component.key} provides a styled context`
1651
+ return bailout(input, 'local/unsupported-target', reason, input.element.span, {
1652
+ rule: 6,
1653
+ })
1654
+ }
1655
+ // A zero graph has no runtime left to merge a spread into, and a spread the
1656
+ // compiler evaluated is still a prop set it cannot attribute to an author's
1657
+ // style intent. Both shapes are rejected, so the message is one message.
1658
+ if (options.zeroRuntime) {
1659
+ const spread = input.element.entries.find((entry) => entry.kind === 'spread')
1660
+ if (spread) {
1661
+ return bailout(
1662
+ input,
1663
+ 'local/unsafe-style-spread',
1664
+ 'Zero-runtime rejects prop spreads',
1665
+ spread.span,
1666
+ {
1667
+ rule: 1,
1668
+ message: zeroRuleMessage(1, { component: input.element.component.name }),
1669
+ }
1670
+ )
1671
+ }
1672
+ }
1673
+ const props: Record<string, unknown> = {}
1674
+ for (const entry of input.element.entries) {
1675
+ if (entry.kind === 'child' || entry.value.kind !== 'static') continue
1676
+ if (entry.kind === 'spread') {
1677
+ if (!staticObject(entry.value.value)) {
1678
+ return bailout(
1679
+ input,
1680
+ 'local/unsafe-style-spread',
1681
+ 'Static spread did not materialize to an object',
1682
+ entry.span
1683
+ )
1684
+ }
1685
+ Object.assign(props, entry.value.value)
1686
+ } else {
1687
+ props[entry.name] = entry.value.value
1688
+ }
1689
+ }
1690
+ if (component.staticConfig.resolvers?.length) {
1691
+ const unresolvedProps = input.element.entries.filter(
1692
+ (entry) => entry.kind === 'prop' && entry.value.kind !== 'static'
1693
+ )
1694
+ // One conditional with static leaves is still a complete prop set:
1695
+ // lowerCandidate evaluates the full resolver chain once per branch.
1696
+ // Multiple unknown props would require a cross-product/read set, so
1697
+ // they remain on the runtime path.
1698
+ const onlyStaticBranchConditional =
1699
+ unresolvedProps.length === 1 &&
1700
+ unresolvedProps[0]?.kind === 'prop' &&
1701
+ unresolvedProps[0].value.kind === 'conditional' &&
1702
+ canLowerConditionalStyleProp(unresolvedProps[0].name, component)
1703
+ const unresolvedProp = onlyStaticBranchConditional
1704
+ ? undefined
1705
+ : unresolvedProps[0]
1706
+ if (unresolvedProp?.kind === 'prop') {
1707
+ return bailout(
1708
+ input,
1709
+ 'local/dynamic-style-value',
1710
+ `Component resolver prop ${unresolvedProp.name} could not be evaluated`,
1711
+ unresolvedProp.span,
1712
+ undefined,
1713
+ unresolvedProp.name
1714
+ )
1715
+ }
1716
+ }
1717
+ // asChild, disableOptimization and themeInverse each asked only whether the
1718
+ // prop was PRESENT, so a false value retained a component that requested
1719
+ // none of the runtime behavior the branch exists for. read the materialized
1720
+ // value instead, and treat a prop the compiler could not evaluate as active.
1721
+ // a later duplicate wins, so the last entry decides and any unevaluated
1722
+ // entry for that name makes the answer unknown.
1723
+ const componentOnlyProp = (name: string) => {
1724
+ let span: MaterializedElement['entries'][number]['span'] | undefined
1725
+ let unknown = false
1726
+ for (const entry of input.element.entries) {
1727
+ if (entry.kind !== 'prop' || entry.name !== name) continue
1728
+ span = entry.span
1729
+ if (entry.value.kind !== 'static') unknown = true
1730
+ }
1731
+ return { unknown, span }
1732
+ }
1733
+ const disableOptimization = componentOnlyProp('disableOptimization')
1734
+ if (disableOptimization.unknown || props.disableOptimization) {
1735
+ return bailout(
1736
+ input,
1737
+ 'local/unsupported-target',
1738
+ 'disableOptimization keeps the component on the runtime path',
1739
+ disableOptimization.span
1740
+ )
1741
+ }
1742
+ if (component.domTag && props.hidden) props.display = 'none'
1743
+ if (component.domTag && platform === 'native') {
1744
+ for (const [name, styleKey] of DOM_STYLE_ATTRIBUTES) {
1745
+ if (name in props) {
1746
+ props[styleKey] = props[name]
1747
+ delete props[name]
1748
+ }
1749
+ }
1750
+ }
1751
+ if (
1752
+ platform === 'native' &&
1753
+ component.domTag === 'input' &&
1754
+ typeof props.type === 'string' &&
1755
+ !NATIVE_INPUT_TYPES.includes(props.type)
1756
+ ) {
1757
+ return bailout(
1758
+ input,
1759
+ 'local/unsupported-target',
1760
+ `input type ${props.type} has no native text-entry control`
1761
+ )
1762
+ }
1763
+ // `style={{ width: expr, height: 10 }}`: the static members stay one
1764
+ // style object, and each dynamic member lowers like the direct prop of
1765
+ // its name
1766
+ let styleObjectStatic: Record<string, unknown> | null = null
1767
+ const styleMemberEntries: DynamicStyleEntry[] = []
1768
+ for (const entry of input.element.entries) {
1769
+ if (
1770
+ entry.kind !== 'prop' ||
1771
+ entry.name !== 'style' ||
1772
+ entry.value.kind !== 'object'
1773
+ ) {
1774
+ continue
1775
+ }
1776
+ styleObjectStatic = {}
1777
+ for (const member of entry.value.members) {
1778
+ if (member.value.kind === 'static') {
1779
+ styleObjectStatic[member.name] = member.value.value
1780
+ continue
1781
+ }
1782
+ if (!isStyleProp(member.name, component)) {
1783
+ return bailout(
1784
+ input,
1785
+ 'local/dynamic-style-value',
1786
+ `Style prop ${member.name} could not be evaluated`,
1787
+ member.span
1788
+ )
1789
+ }
1790
+ styleMemberEntries.push({
1791
+ kind: 'prop',
1792
+ name: member.name,
1793
+ span: member.span,
1794
+ value: member.value,
1795
+ styleMember: true,
1796
+ })
1797
+ }
1798
+ if (Object.keys(styleObjectStatic).length === 0) styleObjectStatic = null
1799
+ }
1800
+ if (styleObjectStatic) props.style = styleObjectStatic
1801
+ const dynamicStyleEntries: DynamicStyleEntry[] = [
1802
+ ...input.element.entries.filter(
1803
+ (entry): entry is DynamicStyleEntry =>
1804
+ entry.kind === 'prop' &&
1805
+ (entry.value.kind === 'bailout' || entry.value.kind === 'conditional') &&
1806
+ isStyleProp(entry.name, component)
1807
+ ),
1808
+ ...styleMemberEntries,
1809
+ ]
1810
+ // both platforms: web needs runtime event mapping, and a flattened bare
1811
+ // RN View silently ignores onPress/onLongPress (Tamagui wires press via
1812
+ // its responder system at runtime)
1813
+ {
1814
+ // DOM primitives (html.*) keep their own event contract (see
1815
+ // @tamagui/dom events table) — only bail for tamagui components,
1816
+ // whose pointer handlers exist solely via usePointerEvents.
1817
+ const needsRuntimeMapping = (name: string) =>
1818
+ runtimeEventProps.has(name) ||
1819
+ (platform === 'native' &&
1820
+ !component.domTag &&
1821
+ nativePointerEventProps.has(name))
1822
+ const directRuntimeEvent = input.element.entries.find(
1823
+ (entry) => entry.kind === 'prop' && needsRuntimeMapping(entry.name)
1824
+ )
1825
+ const runtimeEvent =
1826
+ directRuntimeEvent?.kind === 'prop'
1827
+ ? directRuntimeEvent.name
1828
+ : Object.keys(props).find(needsRuntimeMapping)
1829
+ if (runtimeEvent) {
1830
+ return bailout(
1831
+ input,
1832
+ 'local/unsupported-target',
1833
+ `${runtimeEvent} requires Tamagui runtime event mapping`
1834
+ )
1835
+ }
1836
+ }
1837
+ // A styled() definition's animation props decide the same things the call
1838
+ // site's do, so the lowering decision reads both. completeProps merges
1839
+ // them far below, long after this point, and reading only the call site
1840
+ // here decided a definition's transition/animation props as if they
1841
+ // had never been written: the element flattened and the prop was dropped
1842
+ // with no diagnostic. Same merge function and same precedence as
1843
+ // completeProps, so there is one answer to "what is this prop's value".
1844
+ const animationDefaultProps = component.staticConfig.defaultProps ?? {}
1845
+ const animationProps = core.mergeProps(animationDefaultProps, props)
1846
+ const animationNames = new Set([
1847
+ ...input.element.entries.flatMap((entry) =>
1848
+ entry.kind === 'prop' && runtimeAnimationProps.has(entry.name)
1849
+ ? [entry.name]
1850
+ : []
1851
+ ),
1852
+ ...Object.keys(animationProps).filter(
1853
+ (name) => runtimeAnimationProps.has(name) && animationProps[name] !== undefined
1854
+ ),
1855
+ ])
1856
+ const transitionEntry = input.element.entries.find(
1857
+ (entry) => entry.kind === 'prop' && entry.name === 'transition'
1858
+ )
1859
+ const animatedBy =
1860
+ typeof animationProps.animatedBy === 'string'
1861
+ ? animationProps.animatedBy.trim()
1862
+ : null
1863
+ const namedAnimationDriver =
1864
+ animatedBy === null ? null : options.tamaguiConfig.animationDrivers?.[animatedBy]
1865
+ const namedCssAnimationDriver =
1866
+ platform === 'web' &&
1867
+ namedAnimationDriver &&
1868
+ !namedAnimationDriver.isStub &&
1869
+ namedAnimationDriver.outputStyle === 'css'
1870
+ ? (namedAnimationDriver as AnimationDriver)
1871
+ : null
1872
+ const cssAnimationDriver =
1873
+ namedCssAnimationDriver ??
1874
+ (animatedBy === null || animatedBy === 'default'
1875
+ ? configuredCssAnimationDriver
1876
+ : null)
1877
+ const resolvedCssTransition =
1878
+ animationNames.has('transition') && cssAnimationDriver
1879
+ ? resolveStaticCssTransition(
1880
+ animationProps.transition,
1881
+ cssAnimationDriver.animations ?? {}
1882
+ )
1883
+ : null
1884
+ // onto props, not animationProps: props is what completeProps merges over
1885
+ // the defaults, so this is also how a resolved definition transition wins
1886
+ // over the preset name the definition wrote
1887
+ if (resolvedCssTransition !== null) {
1888
+ props.transition = resolvedCssTransition
1889
+ }
1890
+ const animatedByNeedsRuntime =
1891
+ animationNames.has('animatedBy') &&
1892
+ !animationNames.has('transition') &&
1893
+ (dynamicStyleEntries.length > 0 ||
1894
+ Object.keys(animationProps).some(
1895
+ (name) =>
1896
+ name !== 'animatedBy' &&
1897
+ ((isStyleProp(name, component) &&
1898
+ typeof animationProps[name] === 'string' &&
1899
+ animationProps[name].includes(':')) ||
1900
+ name === 'animationConfig' ||
1901
+ name === 'forceStyle' ||
1902
+ name === 'onTransition')
1903
+ ))
1904
+ const runtimeAnimationRequired =
1905
+ (animationNames.has('transition') && resolvedCssTransition === null) ||
1906
+ [...animationNames].some(
1907
+ (name) => name !== 'transition' && name !== 'animatedBy'
1908
+ ) ||
1909
+ animatedByNeedsRuntime
1910
+ let dynamicHostStyleProperties: string[] | null = null
1911
+ // the entries lane A carries as inline style: all of them when they all
1912
+ // lower that way, else the ones that do not lower per branch as classes,
1913
+ // so `width={w * 2}` shares an element with `bg={cond ? 'red' : 'blue'}`
1914
+ let laneAEntries: DynamicStyleEntry[] = []
1915
+ const laneAOwners = new Set<string>()
1916
+ // the CSS properties lane A took from the element's style object. the
1917
+ // style layer is the runtime's highest tier, and an inline style wins
1918
+ // over a class in CSS, so a class on the same property may coexist
1919
+ const laneAMemberOwners = new Set<string>()
1920
+ // a static value and an inline dynamic one interact unless the dynamic
1921
+ // one is a style member on the very same property
1922
+ const conflictsWithInline = (
1923
+ owners: Set<string>,
1924
+ dynamicOwners: Set<string>,
1925
+ memberOwners: Set<string>
1926
+ ) =>
1927
+ [...owners].some(
1928
+ (owner) =>
1929
+ !memberOwners.has(owner) && cssOwnersConflict(new Set([owner]), dynamicOwners)
1930
+ )
1931
+ const laneAAttempts = [dynamicStyleEntries]
1932
+ {
1933
+ const rest = dynamicStyleEntries.filter(
1934
+ (entry) => !conditionalClassEntry(entry, component)
1935
+ )
1936
+ if (rest.length > 0 && rest.length < dynamicStyleEntries.length) {
1937
+ laneAAttempts.push(rest)
1938
+ }
1939
+ }
1940
+ if (
1941
+ platform === 'web' &&
1942
+ !options.disablePartialExtraction &&
1943
+ (input.element.form === 'jsx' || input.element.propsSpan !== null) &&
1944
+ dynamicStyleEntries.length > 0 &&
1945
+ !input.element.entries.some(
1946
+ (entry) => entry.kind === 'spread' && entry.value.kind !== 'static'
1947
+ )
1948
+ ) {
1949
+ for (const attempt of laneAAttempts) {
1950
+ const seen = new Set<string>()
1951
+ const properties: string[] = []
1952
+ const dynamicOwners = new Set<string>()
1953
+ const memberOwners = new Set<string>()
1954
+ for (const entry of attempt) {
1955
+ if (
1956
+ entry.kind !== 'prop' ||
1957
+ (entry.value.kind !== 'bailout' && entry.value.kind !== 'conditional')
1958
+ ) {
1959
+ continue
1960
+ }
1961
+ const name = directStyleName(entry.name, component)
1962
+ if (!name || seen.has(name)) {
1963
+ properties.length = 0
1964
+ break
1965
+ }
1966
+ const owners = dynamicStyleOwners(
1967
+ name,
1968
+ component.staticConfig as StaticConfig
1969
+ )
1970
+ if (!owners) {
1971
+ properties.length = 0
1972
+ break
1973
+ }
1974
+ let property: string | null = null
1975
+ const expression = input.source.slice(
1976
+ entry.value.span.start,
1977
+ entry.value.span.end
1978
+ )
1979
+ if (
1980
+ resolvedCssTransition !== null &&
1981
+ (name === 'opacity' || name === 'scale')
1982
+ ) {
1983
+ property =
1984
+ name === 'opacity'
1985
+ ? `opacity: (${expression})`
1986
+ : `transform: "scale(" + (${expression}) + ")"`
1987
+ } else if (entry.value.kind === 'bailout' && owners.has(name)) {
1988
+ const dynamic = entry.value.dynamic
1989
+ if (dynamic?.type === 'number') {
1990
+ property = `${JSON.stringify(name)}: (${expression})`
1991
+ } else if (dynamic?.type === 'string' && dynamic.values?.length) {
1992
+ let valuesStayLiteral = true
1993
+ for (const value of dynamic.values) {
1994
+ const split = resolveSplitStyles(
1995
+ { [entry.name]: value },
1996
+ partialStaticConfig(component.staticConfig as StaticConfig)
1997
+ )
1998
+ const atomics = Object.values(split?.rulesToInsert ?? {}) as any[]
1999
+ if (
2000
+ atomics.length !== 1 ||
2001
+ atomics[0]?.[StyleObjectProperty] !== name ||
2002
+ atomics[0]?.[StyleObjectValue] !== value
2003
+ ) {
2004
+ valuesStayLiteral = false
2005
+ break
2006
+ }
2007
+ }
2008
+ if (valuesStayLiteral) {
2009
+ property = `${JSON.stringify(name)}: (${expression})`
2010
+ }
2011
+ }
2012
+ }
2013
+ if (!property) {
2014
+ properties.length = 0
2015
+ break
2016
+ }
2017
+ seen.add(name)
2018
+ for (const owner of owners) {
2019
+ dynamicOwners.add(owner)
2020
+ if (entry.styleMember) memberOwners.add(owner)
2021
+ }
2022
+ properties.push(property)
2023
+ }
2024
+ if (
2025
+ properties.length === attempt.length &&
2026
+ !input.element.entries.some((entry) => {
2027
+ if (entry.kind !== 'prop' || entry.value.kind !== 'static') return false
2028
+ const name = directStyleName(entry.name, component)
2029
+ if (!name) return false
2030
+ const owners = styleOwners(
2031
+ name,
2032
+ entry.value.value,
2033
+ component.staticConfig as StaticConfig
2034
+ )
2035
+ return !!owners && conflictsWithInline(owners, dynamicOwners, memberOwners)
2036
+ }) &&
2037
+ // the static members of the style object outrank every direct
2038
+ // prop at runtime, so an inline dynamic direct prop on the same
2039
+ // property would invert them
2040
+ !Object.entries(staticObject(props.style) ? props.style : {}).some(
2041
+ ([name, value]) => {
2042
+ const owners = styleOwners(
2043
+ name,
2044
+ value,
2045
+ component.staticConfig as StaticConfig
2046
+ )
2047
+ return !!owners && cssOwnersConflict(owners, dynamicOwners)
2048
+ }
2049
+ )
2050
+ ) {
2051
+ dynamicHostStyleProperties = properties
2052
+ laneAEntries = attempt
2053
+ for (const owner of dynamicOwners) laneAOwners.add(owner)
2054
+ for (const owner of memberOwners) laneAMemberOwners.add(owner)
2055
+ break
2056
+ }
2057
+ }
2058
+ }
2059
+ // web per-branch conditional classes need the same preconditions the
2060
+ // native per-branch path has, and take precedence over web partial
2061
+ // extraction because a full flatten beats a runtime component
2062
+ const laneACoversAll =
2063
+ dynamicHostStyleProperties !== null &&
2064
+ laneAEntries.length === dynamicStyleEntries.length
2065
+ // the entries that lower per branch as classes: all of them, or the ones
2066
+ // lane A left behind
2067
+ const webClassEntries =
2068
+ dynamicHostStyleProperties === null
2069
+ ? dynamicStyleEntries
2070
+ : dynamicStyleEntries.filter((entry) => !laneAEntries.includes(entry))
2071
+ const supportsWebConditionalClasses =
2072
+ platform === 'web' &&
2073
+ !options.disablePartialExtraction &&
2074
+ (input.element.form === 'jsx' || input.element.propsSpan !== null) &&
2075
+ !laneACoversAll &&
2076
+ webClassEntries.length > 0 &&
2077
+ webClassEntries.every((entry) => conditionalClassEntry(entry, component))
2078
+ // a theme name of any value selects a theme, while themeInverse only opens a
2079
+ // boundary when it is on
2080
+ const themeEntry = componentOnlyProp('theme')
2081
+ const themeInverseEntry = componentOnlyProp('themeInverse')
2082
+ const themeBoundary = themeEntry.unknown || 'theme' in props
2083
+ if (themeBoundary || themeInverseEntry.unknown || props.themeInverse) {
2084
+ const themeProp = themeBoundary ? 'theme' : 'themeInverse'
2085
+ return bailout(
2086
+ input,
2087
+ 'local/unsupported-target',
2088
+ 'Theme boundary candidates remain on the runtime path',
2089
+ input.element.span,
2090
+ {
2091
+ rule: 4,
2092
+ message: zeroThemeBoundaryMessage(input.element.component.name, themeProp),
2093
+ }
2094
+ )
2095
+ }
2096
+ // asChild makes createComponent render a Slot: it merges its props into
2097
+ // the single child and emits no element of its own. a flattened host view
2098
+ // would add a wrapper the runtime never renders, and the child would
2099
+ // never receive the merged props. both platforms.
2100
+ const asChild = componentOnlyProp('asChild')
2101
+ if (asChild.unknown || props.asChild) {
2102
+ return bailout(
2103
+ input,
2104
+ 'local/unsupported-target',
2105
+ 'asChild renders a Slot, not a host view',
2106
+ asChild.span
2107
+ )
2108
+ }
2109
+ // group and container props publish separate keys in the same context:
2110
+ // group writes the group name, container writes '@' and '@name'. descendants read it
2111
+ // for `group-name-*` and `@name-*` clauses, so a flattened provider
2112
+ // silently breaks every consumer under it. web compiles these to CSS
2113
+ // container rules instead, so only native needs the bail.
2114
+ if (
2115
+ platform === 'native' &&
2116
+ ('group' in props ||
2117
+ 'container' in props ||
2118
+ 'containerName' in props ||
2119
+ 'containerType' in props)
2120
+ ) {
2121
+ return bailout(
2122
+ input,
2123
+ 'local/unsupported-target',
2124
+ 'Native group and container providers remain on the runtime path'
2125
+ )
2126
+ }
2127
+ if (runtimeAnimationRequired && !cssAnimationDriver) {
2128
+ return bailout(
2129
+ input,
2130
+ 'local/unsupported-target',
2131
+ 'Animated candidates remain on the runtime path',
2132
+ transitionEntry?.span,
2133
+ {
2134
+ rule: 5,
2135
+ message: zeroRuleMessage(5, {
2136
+ detail: `the animation configured on ${input.element.component.name}`,
2137
+ }),
2138
+ }
2139
+ )
2140
+ }
2141
+ if (
2142
+ platform === 'web' &&
2143
+ (dynamicStyleEntries.length > 0 || runtimeAnimationRequired) &&
2144
+ !laneACoversAll &&
2145
+ !supportsWebConditionalClasses &&
2146
+ component.partialRuntimeSafe
2147
+ ) {
2148
+ const hasSpread = input.element.entries.some(
2149
+ (entry) => entry.kind === 'spread' && entry.value.kind !== 'static'
2150
+ )
2151
+ const unsupportedRuntimeStyle = input.element.entries.find(
2152
+ (entry) =>
2153
+ entry.kind === 'prop' &&
2154
+ isStyleProp(entry.name, component) &&
2155
+ !runtimeAnimationProps.has(entry.name) &&
2156
+ !directStyleName(entry.name, component)
2157
+ )
2158
+ if (!hasSpread && !unsupportedRuntimeStyle) {
2159
+ const dynamicOwners = new Set<string>()
2160
+ let canProveDynamicOwnership = true
2161
+ for (const entry of dynamicStyleEntries) {
2162
+ if (entry.kind !== 'prop') continue
2163
+ const name = directStyleName(entry.name, component)
2164
+ if (!name) {
2165
+ canProveDynamicOwnership = false
2166
+ break
2167
+ }
2168
+ const owners = dynamicStyleOwners(
2169
+ name,
2170
+ component.staticConfig as StaticConfig
2171
+ )
2172
+ if (!owners) {
2173
+ canProveDynamicOwnership = false
2174
+ break
2175
+ }
2176
+ for (const owner of owners) dynamicOwners.add(owner)
2177
+ }
2178
+ const staticStyleEntries = canProveDynamicOwnership
2179
+ ? input.element.entries.filter((entry) => {
2180
+ if (entry.kind !== 'prop' || entry.value.kind !== 'static') return false
2181
+ if (runtimeAnimationProps.has(entry.name)) return false
2182
+ const name = directStyleName(entry.name, component)
2183
+ if (!name) return false
2184
+ const owners = styleOwners(
2185
+ name,
2186
+ entry.value.value,
2187
+ component.staticConfig as StaticConfig
2188
+ )
2189
+ return !!owners && !cssOwnersConflict(owners, dynamicOwners)
2190
+ })
2191
+ : []
2192
+ if (staticStyleEntries.length > 0) {
2193
+ const partialProps: Record<string, unknown> = {}
2194
+ for (const entry of staticStyleEntries) {
2195
+ if (entry.kind === 'prop' && entry.value.kind === 'static') {
2196
+ partialProps[entry.name] = entry.value.value
2197
+ }
2198
+ }
2199
+ const partialSplit = resolveSplitStyles(
2200
+ partialProps,
2201
+ partialStaticConfig(component.staticConfig as StaticConfig)
2202
+ )
2203
+ const partialInlineStyle = partialSplit?.viewProps?.style
2204
+ const hasPartialInlineStyle =
2205
+ staticObject(partialInlineStyle) &&
2206
+ Object.keys(partialInlineStyle).length > 0
2207
+ if (partialSplit && !hasPartialInlineStyle) {
2208
+ const artifacts = extractedStyleArtifacts(
2209
+ partialSplit,
2210
+ partialProps,
2211
+ options.tamaguiConfig,
2212
+ false
2213
+ )
2214
+ if (artifacts.className) {
2215
+ if (input.element.form !== 'jsx') {
2216
+ const propsEdits = compiledPropsEdits(
2217
+ input,
2218
+ staticStyleEntries,
2219
+ objectClassName(artifacts.className)
2220
+ )
2221
+ if (propsEdits) {
2222
+ return {
2223
+ ok: true,
2224
+ edits: propsEdits,
2225
+ css: artifacts.css,
2226
+ imports: [],
2227
+ flattened: false,
2228
+ }
2229
+ }
2230
+ } else {
2231
+ const [first, ...rest] = staticStyleEntries
2232
+ return {
2233
+ ok: true,
2234
+ edits: [
2235
+ {
2236
+ start: first!.span.start,
2237
+ end: first!.span.end,
2238
+ content: jsxClassName(artifacts.className),
2239
+ origin: first!.span,
2240
+ },
2241
+ ...rest.map((entry) => ({
2242
+ start: entry.span.start,
2243
+ end: entry.span.end,
2244
+ content: '',
2245
+ origin: entry.span,
2246
+ })),
2247
+ ],
2248
+ css: artifacts.css,
2249
+ imports: [],
2250
+ flattened: false,
2251
+ }
2252
+ }
2253
+ }
2254
+ }
2255
+ }
2256
+ }
2257
+ }
2258
+ if (runtimeAnimationRequired) {
2259
+ return bailout(
2260
+ input,
2261
+ 'local/unsupported-target',
2262
+ 'Animated candidates remain on the runtime path',
2263
+ transitionEntry?.span,
2264
+ {
2265
+ rule: 5,
2266
+ message: zeroRuleMessage(5, {
2267
+ detail: `the animation configured on ${input.element.component.name}`,
2268
+ }),
2269
+ }
2270
+ )
2271
+ }
2272
+ const supportsNativeDynamicStyles =
2273
+ platform === 'native' &&
2274
+ !options.disablePartialExtraction &&
2275
+ (input.element.form === 'jsx' || input.element.propsSpan !== null) &&
2276
+ dynamicStyleEntries.every(
2277
+ (entry) =>
2278
+ entry.kind === 'prop' &&
2279
+ (directStyleName(entry.name, component) === 'opacity' ||
2280
+ (entry.value.kind === 'conditional' &&
2281
+ canLowerConditionalStyleProp(entry.name, component)))
2282
+ )
2283
+ if (
2284
+ dynamicStyleEntries.length > 0 &&
2285
+ !laneACoversAll &&
2286
+ !supportsNativeDynamicStyles &&
2287
+ !supportsWebConditionalClasses
2288
+ ) {
2289
+ const entry = dynamicStyleEntries[0]!
2290
+ return bailout(
2291
+ input,
2292
+ 'local/dynamic-style-value',
2293
+ `Style prop ${entry.kind === 'prop' ? entry.name : 'unknown'} could not be safely extracted`,
2294
+ entry.span
2295
+ )
2296
+ }
2297
+ const resolvedStyleStaticConfig = core.getStyleStaticConfig(
2298
+ component.staticConfig,
2299
+ core.getConfig()
2300
+ )
2301
+ const staticDefaultProps = resolvedStyleStaticConfig.defaultProps ?? {}
2302
+ const defaultProps =
2303
+ platform === 'web' &&
2304
+ !component.staticConfig.isText &&
2305
+ options.tamaguiConfig.settings.defaultPosition === 'relative' &&
2306
+ staticDefaultProps.position === undefined
2307
+ ? core.mergeProps({ position: 'relative' }, staticDefaultProps)
2308
+ : staticDefaultProps
2309
+ let completeProps = core.mergeProps(defaultProps, props)
2310
+ if (platform === 'native' && component.domTag && props.display === 'flex') {
2311
+ completeProps = core.mergeProps(
2312
+ core.mergeProps(defaultProps, NATIVE_FLEX_DEFAULTS),
2313
+ props
2314
+ )
2315
+ }
2316
+ const domStyleProgram = input.element.entries.find(
2317
+ (entry) =>
2318
+ entry.kind === 'prop' &&
2319
+ entry.name === 'style' &&
2320
+ entry.value.kind === 'dom-style'
2321
+ )
2322
+ if (platform === 'native' && !component.domTag && domStyleProgram) {
2323
+ return bailout(
2324
+ input,
2325
+ 'local/unsupported-target',
2326
+ 'Native style() pieces remain on the runtime path'
2327
+ )
2328
+ }
2329
+ const propsForConditional = (target: DynamicStyleEntry, value: unknown) => {
2330
+ const branchProps: Record<string, unknown> = {}
2331
+ if (styleObjectStatic) branchProps.style = styleObjectStatic
2332
+ if (target.styleMember) {
2333
+ branchProps.style = { ...styleObjectStatic, [target.name]: value }
2334
+ }
2335
+ for (const entry of input.element.entries) {
2336
+ if (entry === target) {
2337
+ branchProps[target.name] = value
2338
+ continue
2339
+ }
2340
+ if (entry.kind === 'child' || entry.value.kind !== 'static') continue
2341
+ if (entry.kind === 'spread') {
2342
+ if (staticObject(entry.value.value))
2343
+ Object.assign(branchProps, entry.value.value)
2344
+ } else {
2345
+ branchProps[entry.name] = entry.value.value
2346
+ }
2347
+ }
2348
+ if (component.domTag && branchProps.hidden) branchProps.display = 'none'
2349
+ if (resolvedCssTransition !== null) {
2350
+ branchProps.transition = resolvedCssTransition
2351
+ }
2352
+ if (component.domTag && platform === 'native') {
2353
+ for (const [name, styleKey] of DOM_STYLE_ATTRIBUTES) {
2354
+ if (name in branchProps) {
2355
+ branchProps[styleKey] = branchProps[name]
2356
+ delete branchProps[name]
2357
+ }
2358
+ }
2359
+ }
2360
+ const branchCompleteProps =
2361
+ platform === 'native' && component.domTag && branchProps.display === 'flex'
2362
+ ? core.mergeProps(
2363
+ core.mergeProps(defaultProps, NATIVE_FLEX_DEFAULTS),
2364
+ branchProps
2365
+ )
2366
+ : core.mergeProps(defaultProps, branchProps)
2367
+ // the same platform-clause reduction the unconditional props get: a
2368
+ // branch resolves through the full pipeline, so a clause left standing
2369
+ // here would be decided by the build machine instead of the bundle
2370
+ if (platform === 'native') {
2371
+ for (const [name, value] of Object.entries(branchCompleteProps)) {
2372
+ const clause = isStyleProp(name, component) ? nativeClauseValue(value) : null
2373
+ if (clause === null || clause === 'live') continue
2374
+ if (clause.matched) branchCompleteProps[name] = clause.payload
2375
+ else delete branchCompleteProps[name]
2376
+ }
2377
+ }
2378
+ return { branchProps, branchCompleteProps }
2379
+ }
2380
+ // native resolution evaluates clauses against the build machine's current
2381
+ // state, so folding a call-site, styled base, or variant clause would freeze
2382
+ // that state into the bundle.
2383
+ if (platform === 'native') {
2384
+ const isClauseValue = (name: string, value: unknown) =>
2385
+ isStyleProp(name, component) &&
2386
+ ((typeof value === 'string' && flatClausePattern.test(value)) ||
2387
+ isClauseObjectValue(value))
2388
+ // a clause can also sit inside a variant definition, where it never
2389
+ // appears as a prop: variants: { big: { true: { width: 'gt-lg:999px' } } }.
2390
+ // those live in the styled definition, which this element cannot rewrite,
2391
+ // so a clause there retains whatever kind it is.
2392
+ const defaultVariants = component.staticConfig.defaultVariants ?? {}
2393
+ const definitionCarriesClause =
2394
+ Object.entries(resolvedStyleStaticConfig.baseStyle ?? {}).some(
2395
+ ([name, value]) => isClauseValue(name, value)
2396
+ ) ||
2397
+ Object.entries(component.staticConfig.variants ?? {}).some(
2398
+ ([variantName, definitions]) =>
2399
+ (completeProps[variantName] !== undefined ||
2400
+ defaultVariants[variantName] !== undefined) &&
2401
+ staticObject(definitions) &&
2402
+ Object.values(definitions).some(
2403
+ (definition) =>
2404
+ staticObject(definition) &&
2405
+ Object.entries(definition).some(([name, value]) =>
2406
+ isClauseValue(name, value)
2407
+ )
2408
+ )
2409
+ )
2410
+ if (definitionCarriesClause) {
2411
+ return bailout(
2412
+ input,
2413
+ 'local/unsupported-target',
2414
+ 'Native conditional value programs remain on the runtime path'
2415
+ )
2416
+ }
2417
+ const reducedProps: Record<string, unknown> = {}
2418
+ for (const [name, value] of Object.entries(completeProps)) {
2419
+ const clause = isStyleProp(name, component) ? nativeClauseValue(value) : null
2420
+ if (clause === 'live') {
2421
+ return bailout(
2422
+ input,
2423
+ 'local/unsupported-target',
2424
+ 'Native conditional value programs remain on the runtime path'
2425
+ )
2426
+ }
2427
+ if (clause === null) {
2428
+ reducedProps[name] = value
2429
+ continue
2430
+ }
2431
+ if (clause.matched) reducedProps[name] = clause.payload
2432
+ }
2433
+ completeProps = reducedProps
2434
+ // a conditional leaf is a clause program the same way a direct prop is,
2435
+ // and propsForConditional reduces the ones that fold
2436
+ for (const entry of dynamicStyleEntries) {
2437
+ if (entry.value.kind !== 'conditional') continue
2438
+ for (const leaf of collectLeaves(entry.value.tree)) {
2439
+ if (nativeClauseValue(leaf.value) !== 'live') continue
2440
+ return bailout(
2441
+ input,
2442
+ 'local/unsupported-target',
2443
+ 'Native conditional value programs remain on the runtime path'
2444
+ )
2445
+ }
2446
+ }
2447
+ }
2448
+ const split = resolveSplitStyles(
2449
+ completeProps,
2450
+ component.staticConfig,
2451
+ cssAnimationDriver,
2452
+ component.displayName
2453
+ )
2454
+ if (!split) {
2455
+ return bailout(
2456
+ input,
2457
+ 'local/style-resolution-failed',
2458
+ 'getSplitStyles returned no static result'
2459
+ )
2460
+ }
2461
+ if (
2462
+ split.programLifecycleStyleKeys?.enter?.size ||
2463
+ split.programLifecycleStyleKeys?.exit?.size
2464
+ ) {
2465
+ return bailout(
2466
+ input,
2467
+ 'local/unsupported-target',
2468
+ 'Lifecycle value programs remain on the runtime path',
2469
+ input.element.span,
2470
+ {
2471
+ rule: 5,
2472
+ message: zeroRuleMessage(5, {
2473
+ detail: `an enter or exit style program on ${input.element.component.name}`,
2474
+ }),
2475
+ }
2476
+ )
2477
+ }
2478
+ const flatTag =
2479
+ component.domTag ??
2480
+ (typeof props.render === 'string'
2481
+ ? props.render
2482
+ : typeof defaultProps.render === 'string'
2483
+ ? defaultProps.render
2484
+ : component.staticConfig.isText
2485
+ ? 'span'
2486
+ : 'div')
2487
+ const tagEdits = [input.element.component.span, input.element.component.closingSpan]
2488
+ .filter((span): span is NonNullable<typeof span> => !!span)
2489
+ .map((span) => ({
2490
+ start: span.start,
2491
+ end: span.end,
2492
+ content: input.element.form === 'jsx' ? flatTag : JSON.stringify(flatTag),
2493
+ origin: span,
2494
+ }))
2495
+
2496
+ const isPropIgnored = (name: string) =>
2497
+ isStyleProp(name, component) ||
2498
+ isInvalidHostStyleProp(name, component) ||
2499
+ componentOnlyProps.has(name)
2500
+ const spreadReplacement = (
2501
+ form: MaterializedElement['form'],
2502
+ entry: MaterializedElement['entries'][number]
2503
+ ) =>
2504
+ spreadNonStyleReplacement(form, entry, isPropIgnored, (name, value) => {
2505
+ if (platform !== 'web') return [name, value]
2506
+ if (name === 'testID') return ['data-testid', value]
2507
+ if (component.domTag && name === 'for') return ['htmlFor', value]
2508
+ if (component.domTag && name === 'role' && value === 'none') {
2509
+ return ['role', 'presentation']
2510
+ }
2511
+ return [name, value]
2512
+ })
2513
+
2514
+ let styleEntries = input.element.entries.filter(
2515
+ (entry) =>
2516
+ (entry.kind === 'prop' && isPropIgnored(entry.name)) ||
2517
+ (entry.kind === 'spread' &&
2518
+ entry.value.kind === 'static' &&
2519
+ staticObject(entry.value.value) &&
2520
+ Object.keys(entry.value.value).some(isPropIgnored))
2521
+ )
2522
+ let invalidHostStyle:
2523
+ | { entry: MaterializedElement['entries'][number]; name: string }
2524
+ | undefined
2525
+ for (const entry of input.element.entries) {
2526
+ if (entry.kind === 'prop') {
2527
+ if (isInvalidHostStyleProp(entry.name, component)) {
2528
+ invalidHostStyle = { entry, name: entry.name }
2529
+ break
2530
+ }
2531
+ continue
2532
+ }
2533
+ if (
2534
+ entry.kind === 'spread' &&
2535
+ entry.value.kind === 'static' &&
2536
+ staticObject(entry.value.value)
2537
+ ) {
2538
+ const name = Object.keys(entry.value.value).find((name) =>
2539
+ isInvalidHostStyleProp(name, component)
2540
+ )
2541
+ if (name) {
2542
+ invalidHostStyle = { entry, name }
2543
+ break
2544
+ }
2545
+ }
2546
+ }
2547
+ if (invalidHostStyle) {
2548
+ return bailout(
2549
+ input,
2550
+ 'local/unsupported-target',
2551
+ `"${invalidHostStyle.name}" is a text style prop and this component is not text. Use a Text-based component, or html.* for raw web elements.`,
2552
+ invalidHostStyle.entry.span
2553
+ )
2554
+ }
2555
+ if (platform === 'native' && component.domTag) {
2556
+ const mixedSpread = styleEntries.find(
2557
+ (entry) =>
2558
+ entry.kind === 'spread' &&
2559
+ entry.value.kind === 'static' &&
2560
+ staticObject(entry.value.value) &&
2561
+ Object.keys(entry.value.value).some((name) => !isPropIgnored(name))
2562
+ )
2563
+ if (mixedSpread) {
2564
+ return bailout(
2565
+ input,
2566
+ 'local/unsafe-style-spread',
2567
+ 'Native DOM prop mapping requires non-style spread props to remain on the runtime path',
2568
+ mixedSpread.span
2569
+ )
2570
+ }
2571
+ }
2572
+ const webPropEdits: SourceEdit[] =
2573
+ platform === 'web'
2574
+ ? input.element.entries.flatMap((entry) => {
2575
+ if (entry.kind !== 'prop' || entry.name !== 'testID') return []
2576
+ const content = input.source.slice(entry.span.start, entry.span.end)
2577
+ const nameOffset = content.indexOf('testID')
2578
+ if (nameOffset === -1) return []
2579
+ const alreadyQuoted =
2580
+ content[nameOffset - 1] === '"' || content[nameOffset - 1] === "'"
2581
+ return [
2582
+ {
2583
+ start: entry.span.start + nameOffset,
2584
+ end: entry.span.start + nameOffset + 'testID'.length,
2585
+ content:
2586
+ input.element.form === 'jsx' || alreadyQuoted
2587
+ ? 'data-testid'
2588
+ : `'data-testid'`,
2589
+ origin: entry.span,
2590
+ },
2591
+ ]
2592
+ })
2593
+ : []
2594
+ const webDOMResult =
2595
+ platform === 'web' && component.domTag
2596
+ ? webDOMProps(input, component.domTag)
2597
+ : { edits: [] as SourceEdit[], additions: [] as [string, string][] }
2598
+ webPropEdits.push(...webDOMResult.edits)
2599
+
2600
+ if (platform === 'native') {
2601
+ const nativeStyleResolved = split.viewProps?.style
2602
+ // Theme-backed values arrive as sentinel strings under
2603
+ // resolveValues: 'except-theme'. Split them out the way V2's
2604
+ // extractToNative did: plain styles stay hoisted and static, themed
2605
+ // keys read the live theme through _withStableStyle so a flattened
2606
+ // component still updates when the theme changes.
2607
+ let themedStyleKeys: Record<string, string> | null = null
2608
+ let nativeStyle = nativeStyleResolved
2609
+ if (staticObject(nativeStyleResolved)) {
2610
+ for (const [styleKey, styleValue] of Object.entries(nativeStyleResolved)) {
2611
+ if (!core.containsThemeRef(styleValue)) continue
2612
+ const themeKey = core.themeRefKey(styleValue)
2613
+ if (!themeKey) {
2614
+ return bailout(
2615
+ input,
2616
+ 'local/dynamic-style-value',
2617
+ `Style ${styleKey} uses a theme value in a compound or modified position that compiled output cannot represent`
2618
+ )
2619
+ }
2620
+ ;(themedStyleKeys ||= {})[styleKey] = themeKey
2621
+ }
2622
+ if (themedStyleKeys) {
2623
+ const plain: Record<string, unknown> = {}
2624
+ for (const [styleKey, styleValue] of Object.entries(nativeStyleResolved)) {
2625
+ if (!(styleKey in themedStyleKeys)) plain[styleKey] = styleValue
2626
+ }
2627
+ nativeStyle = plain
2628
+ }
2629
+ } else if (core.containsThemeRef(nativeStyleResolved)) {
2630
+ return bailout(
2631
+ input,
2632
+ 'local/dynamic-style-value',
2633
+ 'Theme values inside non-object native style output stay on the runtime path'
2634
+ )
2635
+ }
2636
+ if (!isSerializableNativeStyle(nativeStyle)) {
2637
+ return bailout(
2638
+ input,
2639
+ 'local/unsupported-target',
2640
+ 'Native style output is not a static serializable value'
2641
+ )
2642
+ }
2643
+ const nativeStyleLocal = unusedIdentifier(
2644
+ input.source,
2645
+ `__TamaguiNativeStyle${input.element.span.start}`
2646
+ )
2647
+ const nativeStyleImports = [
2648
+ {
2649
+ content: `\nfunction ${nativeStyleLocal}() { return ${nativeStyleLocal}._ ?? (${nativeStyleLocal}._ = ${JSON.stringify(nativeStyle ?? {})}); } ${nativeStyleLocal}();`,
2650
+ origin: input.element.component.span,
2651
+ },
2652
+ ]
2653
+ const nativeStyleSource = `${nativeStyleLocal}._ ?? ${nativeStyleLocal}()`
2654
+ let nativeFastPath:
2655
+ | {
2656
+ imports: { content: string; origin: MaterializedElement['span'] }[]
2657
+ mappingLocal: string
2658
+ }
2659
+ | undefined
2660
+ if (
2661
+ themedStyleKeys &&
2662
+ options.experimentalNativeFastPath &&
2663
+ dynamicStyleEntries.length === 0 &&
2664
+ !input.element.entries.some((entry) => entry.kind === 'spread')
2665
+ ) {
2666
+ const mappingLocal = unusedIdentifier(
2667
+ input.source,
2668
+ `__TamaguiNativeMapping${input.element.span.start}`
2669
+ )
2670
+ nativeFastPath = {
2671
+ mappingLocal,
2672
+ imports: [
2673
+ {
2674
+ content: `\nconst ${mappingLocal} = ${JSON.stringify(themedStyleKeys)};`,
2675
+ origin: input.element.component.span,
2676
+ },
2677
+ ],
2678
+ }
2679
+ }
2680
+ if (component.domTag) {
2681
+ if (themedStyleKeys) {
2682
+ return bailout(
2683
+ input,
2684
+ 'local/unsupported-target',
2685
+ 'Theme values on DOM-tag native output stay on the runtime path'
2686
+ )
2687
+ }
2688
+ const row = TAGS[component.domTag]
2689
+ const basePrimitive = NATIVE_BACKING[row.backing].primitive
2690
+ let primitive = basePrimitive
2691
+ const propsResult = nativeDOMProps(input, component.domTag)
2692
+ if (propsResult.diagnostic) {
2693
+ return bailout(
2694
+ input,
2695
+ 'local/unsupported-target',
2696
+ propsResult.diagnostic,
2697
+ propsResult.diagnosticSpan
2698
+ )
2699
+ }
2700
+ styleEntries = [...new Set([...styleEntries, ...propsResult.consumed])]
2701
+ let nativeDOMStyleSource = nativeStyleSource
2702
+ let runtimeStyleSource: string | null = null
2703
+ if (
2704
+ domStyleProgram?.kind === 'prop' &&
2705
+ domStyleProgram.value.kind === 'dom-style'
2706
+ ) {
2707
+ const needsRuntime = domStyleProgram.value.items.some(
2708
+ (item) =>
2709
+ item.value.kind === 'static' &&
2710
+ staticObject(item.value.value) &&
2711
+ (Object.keys(item.value.value).some((property) =>
2712
+ nativeRuntimeOnlyStyleProperties.has(property)
2713
+ ) ||
2714
+ Object.values(item.value.value).some(
2715
+ (value) =>
2716
+ (typeof value === 'string' &&
2717
+ (flatClausePattern.test(value) ||
2718
+ nativeInheritedKeywordPattern.test(value))) ||
2719
+ isClauseObjectValue(value)
2720
+ ))
2721
+ )
2722
+ const itemSources: string[] = []
2723
+ for (const item of domStyleProgram.value.items) {
2724
+ if (item.value.kind !== 'static' || !staticObject(item.value.value)) {
2725
+ return bailout(
2726
+ input,
2727
+ 'local/dynamic-style-value',
2728
+ 'Every style() handle in a style array must be statically evaluable',
2729
+ item.value.span
2730
+ )
2731
+ }
2732
+ if (
2733
+ Object.values(item.value.value).some(
2734
+ (value) =>
2735
+ typeof value === 'string' && nativeInitialKeywordPattern.test(value)
2736
+ )
2737
+ ) {
2738
+ return bailout(
2739
+ input,
2740
+ 'local/unsupported-target',
2741
+ 'Native DOM style() does not support the CSS initial keyword, matching the pinned upstream limitation',
2742
+ item.value.span
2743
+ )
2744
+ }
2745
+ let value: string
2746
+ if (needsRuntime) {
2747
+ value = JSON.stringify(item.value.value)
2748
+ } else {
2749
+ const itemSplit = resolveSplitStyles(
2750
+ item.value.value,
2751
+ partialStaticConfig(component.staticConfig)
2752
+ )
2753
+ const itemStyle = itemSplit?.viewProps?.style
2754
+ if (
2755
+ !isSerializableNativeStyle(itemStyle) ||
2756
+ core.containsThemeRef(itemStyle)
2757
+ ) {
2758
+ return bailout(
2759
+ input,
2760
+ 'local/unsupported-target',
2761
+ 'Native style() output is not serializable',
2762
+ item.value.span
2763
+ )
2764
+ }
2765
+ value = JSON.stringify(itemStyle ?? {})
2766
+ }
2767
+ const condition = item.condition
2768
+ ? input.source.slice(item.condition.start, item.condition.end)
2769
+ : null
2770
+ itemSources.push(condition ? `(${condition}) && ${value}` : value)
2771
+ }
2772
+ if (needsRuntime) {
2773
+ primitive = `DOMRuntime${basePrimitive.slice('DOM'.length)}`
2774
+ runtimeStyleSource = `[${itemSources.join(', ')}]`
2775
+ } else {
2776
+ nativeDOMStyleSource = `[${[nativeDOMStyleSource, ...itemSources].join(', ')}]`
2777
+ }
2778
+ }
2779
+ const nativeLocal = unusedIdentifier(input.source, `__Tamagui${primitive}`)
2780
+ const propertyContent = [
2781
+ input.element.form === 'jsx'
2782
+ ? `style={${nativeDOMStyleSource}}`
2783
+ : `style: ${nativeDOMStyleSource}`,
2784
+ runtimeStyleSource
2785
+ ? input.element.form === 'jsx'
2786
+ ? `__styles={${runtimeStyleSource}}`
2787
+ : `__styles: ${runtimeStyleSource}`
2788
+ : '',
2789
+ serializedProps(input.element.form, propsResult.additions),
2790
+ ]
2791
+ .filter(Boolean)
2792
+ .join(input.element.form === 'jsx' ? ' ' : ', ')
2793
+ const primitiveTagEdits = [
2794
+ input.element.component.span,
2795
+ input.element.component.closingSpan,
2796
+ ]
2797
+ .filter((span): span is NonNullable<typeof span> => !!span)
2798
+ .map((span) => ({
2799
+ start: span.start,
2800
+ end: span.end,
2801
+ content:
2802
+ input.element.form === 'jsx'
2803
+ ? nativeLocal
2804
+ : JSON.stringify(nativeLocal).slice(1, -1),
2805
+ origin: span,
2806
+ }))
2807
+ const literalEdits: SourceEdit[] = []
2808
+ const imports = [
2809
+ ...nativeStyleImports,
2810
+ {
2811
+ content: `\nimport { ${primitive} as ${nativeLocal} } from ${JSON.stringify(NATIVE_PRIMITIVE_MODULE)}\n`,
2812
+ origin: input.element.component.span,
2813
+ },
2814
+ ]
2815
+ if (NATIVE_BACKING[row.backing].wrapsLiteralText) {
2816
+ const textLocal = unusedIdentifier(input.source, '__TamaguiDOMText')
2817
+ const createElementLocal = unusedIdentifier(
2818
+ input.source,
2819
+ '__TamaguiCreateElement'
2820
+ )
2821
+ let needsText = false
2822
+ let needsCreateElement = false
2823
+ for (const entry of input.element.entries) {
2824
+ if (entry.kind !== 'child') continue
2825
+ if (
2826
+ entry.value.kind === 'empty' ||
2827
+ entry.value.kind === 'element' ||
2828
+ (entry.value.kind === 'static' &&
2829
+ (entry.value.value === null || typeof entry.value.value === 'boolean'))
2830
+ ) {
2831
+ continue
2832
+ }
2833
+ if (
2834
+ entry.value.kind !== 'static' ||
2835
+ entry.value.literalOrigin !== true ||
2836
+ (typeof entry.value.value !== 'string' &&
2837
+ typeof entry.value.value !== 'number')
2838
+ ) {
2839
+ return bailout(
2840
+ input,
2841
+ 'local/unsupported-child',
2842
+ `html.${component.domTag} has a direct child that may render unwrapped native text; write a literal as JSX text or wrap the child in html.span`,
2843
+ entry.span
2844
+ )
2845
+ }
2846
+ needsText = true
2847
+ const child = input.source.slice(entry.span.start, entry.span.end)
2848
+ if (input.element.form === 'jsx') {
2849
+ literalEdits.push({
2850
+ start: entry.span.start,
2851
+ end: entry.span.end,
2852
+ content: `<${textLocal} __inherit>${child}</${textLocal}>`,
2853
+ origin: entry.span,
2854
+ })
2855
+ } else {
2856
+ needsCreateElement = true
2857
+ literalEdits.push({
2858
+ start: entry.span.start,
2859
+ end: entry.span.end,
2860
+ content: `${createElementLocal}(${textLocal}, { __inherit: true }, ${child})`,
2861
+ origin: entry.span,
2862
+ })
2863
+ }
2864
+ }
2865
+ if (needsText) {
2866
+ imports.push({
2867
+ content: `\nimport { DOMText as ${textLocal} } from ${JSON.stringify(NATIVE_PRIMITIVE_MODULE)}\n`,
2868
+ origin: input.element.component.span,
2869
+ })
2870
+ }
2871
+ if (needsCreateElement) {
2872
+ imports.push({
2873
+ content: `\nimport { createElement as ${createElementLocal} } from "react"\n`,
2874
+ origin: input.element.component.span,
2875
+ })
2876
+ }
2877
+ }
2878
+ const [first, ...rest] = styleEntries
2879
+ const firstNonStyle = first ? spreadReplacement('jsx', first) : ''
2880
+ const propsEdits =
2881
+ input.element.form === 'jsx'
2882
+ ? styleEntries.length === 0
2883
+ ? [
2884
+ {
2885
+ start: input.element.component.span.end,
2886
+ end: input.element.component.span.end,
2887
+ content: ` ${propertyContent}`,
2888
+ origin: input.element.component.span,
2889
+ },
2890
+ ]
2891
+ : [
2892
+ {
2893
+ start: first!.span.start,
2894
+ end: first!.span.end,
2895
+ content: [propertyContent, firstNonStyle].filter(Boolean).join(' '),
2896
+ origin: first!.span,
2897
+ },
2898
+ ...rest.map((entry) => ({
2899
+ start: entry.span.start,
2900
+ end: entry.span.end,
2901
+ content: spreadReplacement('jsx', entry),
2902
+ origin: entry.span,
2903
+ })),
2904
+ ]
2905
+ : compiledPropsEdits(input, styleEntries, propertyContent, (entry) =>
2906
+ spreadReplacement(input.element.form, entry)
2907
+ )
2908
+ if (!propsEdits) {
2909
+ return bailout(
2910
+ input,
2911
+ 'local/unsupported-target',
2912
+ `Compiled ${input.element.form} call has no editable props argument`
2913
+ )
2914
+ }
2915
+ return {
2916
+ ok: true,
2917
+ edits: [
2918
+ ...primitiveTagEdits,
2919
+ ...propsResult.edits,
2920
+ ...propsEdits,
2921
+ ...literalEdits,
2922
+ ],
2923
+ css: [],
2924
+ imports,
2925
+ flattened: true,
2926
+ }
2927
+ }
2928
+ const nativeName = component.staticConfig.isText ? 'Text' : 'View'
2929
+ // RN's <View> is a JS component wrapping the real host component: it
2930
+ // reads TextAncestorContext, remaps ~25 aria/id/tabIndex props, then
2931
+ // renders <ViewNativeComponent>. RN exports that inner component as
2932
+ // `unstable_NativeView`, and its registry returns the component NAME,
2933
+ // so emitting it is literally `createElement('RCTView', props)` and
2934
+ // saves a context read plus a React element per view per render.
2935
+ // Only when the call site needs nothing the wrapper adds: the aria
2936
+ // remapping (visible in the static props) and the TextAncestorContext
2937
+ // reset, which only a child can observe. <Text>'s wrapper does far
2938
+ // more than <View>'s, so text keeps the wrapper.
2939
+ const useHostView =
2940
+ options.experimentalNativeFastPath &&
2941
+ nativeName === 'View' &&
2942
+ isBareHostView(input.element)
2943
+ const nativeExport = useHostView ? 'unstable_NativeView' : nativeName
2944
+ // the local is named after the BINDING, not the component: a file can
2945
+ // hold both kinds at once, and a shared name would emit two different
2946
+ // declarations of one identifier
2947
+ const nativeLocal = unusedIdentifier(
2948
+ input.source,
2949
+ `__TamaguiNative${useHostView ? 'HostView' : nativeName}`
2950
+ )
2951
+ if (themedStyleKeys && options.disablePartialExtraction) {
2952
+ return bailout(
2953
+ input,
2954
+ 'local/dynamic-style-value',
2955
+ 'Theme values require partial extraction, which is disabled'
2956
+ )
2957
+ }
2958
+ if (dynamicStyleEntries.length > 0 || themedStyleKeys) {
2959
+ if (nativeFastPath) {
2960
+ const fastLocal = `__TamaguiNativeFast${nativeName}${input.element.span.start}`
2961
+ const tagEdits = [
2962
+ input.element.component.span,
2963
+ input.element.component.closingSpan,
2964
+ ]
2965
+ .filter((span): span is NonNullable<typeof span> => !!span)
2966
+ .map((span) => ({
2967
+ start: span.start,
2968
+ end: span.end,
2969
+ content: fastLocal,
2970
+ origin: span,
2971
+ }))
2972
+ const [first, ...rest] = styleEntries
2973
+ const styleEdits =
2974
+ styleEntries.length === 0
2975
+ ? []
2976
+ : input.element.form === 'jsx'
2977
+ ? [
2978
+ {
2979
+ start: first!.span.start,
2980
+ end: first!.span.end,
2981
+ content: '',
2982
+ origin: first!.span,
2983
+ },
2984
+ ...rest.map((entry) => ({
2985
+ start: entry.span.start,
2986
+ end: entry.span.end,
2987
+ content: '',
2988
+ origin: entry.span,
2989
+ })),
2990
+ ]
2991
+ : compiledPropsEdits(input, styleEntries, `_expressions: []`)
2992
+ if (!styleEdits) {
2993
+ return bailout(
2994
+ input,
2995
+ 'local/unsupported-target',
2996
+ `Compiled ${input.element.form} call has no editable props argument`
2997
+ )
2998
+ }
2999
+ return {
3000
+ ok: true,
3001
+ edits: [...tagEdits, ...styleEdits],
3002
+ css: [],
3003
+ imports: [
3004
+ ...nativeStyleImports,
3005
+ ...nativeFastPath.imports,
3006
+ {
3007
+ content: `\nconst ${nativeLocal} = require('react-native').${nativeExport};`,
3008
+ origin: input.element.component.span,
3009
+ },
3010
+ {
3011
+ content: `\nconst ${fastLocal} = require('@tamagui/core')._withNativeStyle(${nativeLocal}, ${nativeStyleSource}, ${nativeFastPath.mappingLocal});`,
3012
+ origin: input.element.component.span,
3013
+ },
3014
+ ],
3015
+ flattened: true,
3016
+ }
3017
+ }
3018
+ const stableLocal = `__TamaguiStable${nativeName}${input.element.span.start}`
3019
+ const expressions: string[] = []
3020
+ const plainDynamicParts: string[] = []
3021
+ // V2-parity per-branch lowering: a conditional whose branches
3022
+ // evaluated statically resolves EACH branch through the full style
3023
+ // pipeline at compile time, so cross-key effects (a fontFamily
3024
+ // switch changing size/lineHeight resolution) are captured; only the
3025
+ // test expression survives into the output
3026
+ const conditionalParts: string[] = []
3027
+ const conditionalKeys = new Set<string>()
3028
+ const baseStyleForDiff = staticObject(nativeStyleResolved)
3029
+ ? nativeStyleResolved
3030
+ : {}
3031
+ for (const entry of dynamicStyleEntries) {
3032
+ // opacity keeps the leaner inline-expression form even for
3033
+ // ternaries; per-branch lowering is for props that form cannot carry
3034
+ if (
3035
+ entry.value.kind === 'conditional' &&
3036
+ directStyleName(entry.name, component) !== 'opacity'
3037
+ ) {
3038
+ const tree = entry.value.tree
3039
+ const leaves = collectLeaves(tree)
3040
+ const leafDiffs = new Map<
3041
+ (typeof leaves)[number],
3042
+ Record<string, unknown>
3043
+ >()
3044
+ for (const leaf of leaves) {
3045
+ const { branchCompleteProps } = propsForConditional(entry, leaf.value)
3046
+ const branchSplit = resolveSplitStyles(
3047
+ branchCompleteProps,
3048
+ component.staticConfig,
3049
+ cssAnimationDriver,
3050
+ component.displayName
3051
+ )
3052
+ const branchStyle = branchSplit?.viewProps?.style ?? {}
3053
+ if (!staticObject(branchStyle)) {
3054
+ return bailout(
3055
+ input,
3056
+ 'local/dynamic-style-value',
3057
+ `Conditional ${entry.name} branch did not resolve to a static native style`,
3058
+ entry.value.span
3059
+ )
3060
+ }
3061
+ // a branch (e.g. through a variant) may change viewProps
3062
+ // beyond style; the style array cannot express those, so any
3063
+ // non-style difference sends the element to the runtime path
3064
+ for (const viewPropsKey of new Set([
3065
+ ...Object.keys(branchSplit?.viewProps ?? {}),
3066
+ ...Object.keys(split.viewProps ?? {}),
3067
+ ])) {
3068
+ if (viewPropsKey === 'style') continue
3069
+ if (
3070
+ JSON.stringify(branchSplit?.viewProps?.[viewPropsKey]) !==
3071
+ JSON.stringify((split.viewProps as any)?.[viewPropsKey])
3072
+ ) {
3073
+ return bailout(
3074
+ input,
3075
+ 'local/dynamic-style-value',
3076
+ `Conditional ${entry.name} branch changes ${viewPropsKey}, which the compiled style array cannot express`,
3077
+ entry.value.span
3078
+ )
3079
+ }
3080
+ }
3081
+ for (const key of Object.keys(baseStyleForDiff)) {
3082
+ if (!(key in branchStyle)) {
3083
+ return bailout(
3084
+ input,
3085
+ 'local/dynamic-style-value',
3086
+ `Conditional ${entry.name} branch removes ${key}, which an additive style array cannot express`,
3087
+ entry.value.span
3088
+ )
3089
+ }
3090
+ }
3091
+ const diff: Record<string, unknown> = {}
3092
+ for (const [key, value] of Object.entries(branchStyle)) {
3093
+ if (
3094
+ JSON.stringify((baseStyleForDiff as any)[key]) !==
3095
+ JSON.stringify(value)
3096
+ ) {
3097
+ diff[key] = value
3098
+ }
3099
+ }
3100
+ if (!isSerializableNativeStyle(diff) || core.containsThemeRef(diff)) {
3101
+ return bailout(
3102
+ input,
3103
+ 'local/dynamic-style-value',
3104
+ `Conditional ${entry.name} branch style is not statically representable`,
3105
+ entry.value.span
3106
+ )
3107
+ }
3108
+ for (const key of Object.keys(diff)) {
3109
+ if (conditionalKeys.has(key)) {
3110
+ return bailout(
3111
+ input,
3112
+ 'local/dynamic-style-value',
3113
+ `Multiple conditionals contribute ${key}; their interaction cannot be resolved per-branch`,
3114
+ entry.value.span
3115
+ )
3116
+ }
3117
+ }
3118
+ leafDiffs.set(leaf, diff)
3119
+ }
3120
+ for (const diff of leafDiffs.values()) {
3121
+ for (const key of Object.keys(diff)) conditionalKeys.add(key)
3122
+ }
3123
+
3124
+ function serializeNativeTree(node: BranchDecisionNode): string {
3125
+ if (node.kind === 'leaf') {
3126
+ const diff = leafDiffs.get(node) ?? {}
3127
+ return JSON.stringify(diff)
3128
+ }
3129
+ const index = expressions.length
3130
+ expressions.push(input.source.slice(node.test.start, node.test.end))
3131
+ return `expressions[${index}] ? ${serializeNativeTree(node.whenTrue)} : ${serializeNativeTree(node.whenFalse)}`
3132
+ }
3133
+
3134
+ conditionalParts.push(serializeNativeTree(tree))
3135
+ } else {
3136
+ const index = expressions.length
3137
+ expressions.push(
3138
+ input.source.slice(entry.value.span.start, entry.value.span.end)
3139
+ )
3140
+ plainDynamicParts.push(
3141
+ `${JSON.stringify(directStyleName(entry.name, component))}: expressions[${index}]`
3142
+ )
3143
+ }
3144
+ }
3145
+ const dynamicStyle = plainDynamicParts.join(', ')
3146
+ // themed keys were resolved with the exact theme key known ahead of
3147
+ // time; the wrapper subscribes via useTheme() only when hasThemeKeys
3148
+ // is set, so purely-dynamic elements pay no theme cost
3149
+ const themedStyle = themedStyleKeys
3150
+ ? Object.entries(themedStyleKeys)
3151
+ .map(
3152
+ ([styleKey, themeKey]) =>
3153
+ `${JSON.stringify(styleKey)}: _theme[${JSON.stringify(themeKey)}]?.get()`
3154
+ )
3155
+ .join(', ')
3156
+ : null
3157
+ const styleParts = [
3158
+ nativeStyleSource,
3159
+ ...(themedStyle ? [`{ ${themedStyle} }`] : []),
3160
+ ...conditionalParts,
3161
+ ...(dynamicStyle ? [`{ ${dynamicStyle} }`] : []),
3162
+ ]
3163
+ const tagEdits = [
3164
+ input.element.component.span,
3165
+ input.element.component.closingSpan,
3166
+ ]
3167
+ .filter((span): span is NonNullable<typeof span> => !!span)
3168
+ .map((span) => ({
3169
+ start: span.start,
3170
+ end: span.end,
3171
+ content: stableLocal,
3172
+ origin: span,
3173
+ }))
3174
+ const [first, ...rest] = styleEntries
3175
+ const firstNonStyle = first ? spreadReplacement('jsx', first) : ''
3176
+ const expressionEdits =
3177
+ styleEntries.length === 0
3178
+ ? []
3179
+ : input.element.form === 'jsx'
3180
+ ? [
3181
+ {
3182
+ start: first!.span.start,
3183
+ end: first!.span.end,
3184
+ content: [
3185
+ expressions.length > 0
3186
+ ? `_expressions={[${expressions.join(', ')}]}`
3187
+ : '',
3188
+ firstNonStyle,
3189
+ ]
3190
+ .filter(Boolean)
3191
+ .join(' '),
3192
+ origin: first!.span,
3193
+ },
3194
+ ...rest.map((entry) => ({
3195
+ start: entry.span.start,
3196
+ end: entry.span.end,
3197
+ content: spreadReplacement('jsx', entry),
3198
+ origin: entry.span,
3199
+ })),
3200
+ ]
3201
+ : compiledPropsEdits(
3202
+ input,
3203
+ styleEntries,
3204
+ `_expressions: [${expressions.join(', ')}]`,
3205
+ (entry) => spreadReplacement(input.element.form, entry)
3206
+ )
3207
+ if (!expressionEdits) {
3208
+ return bailout(
3209
+ input,
3210
+ 'local/unsupported-target',
3211
+ `Compiled ${input.element.form} call has no editable props argument`
3212
+ )
3213
+ }
3214
+ return {
3215
+ ok: true,
3216
+ edits: [...tagEdits, ...expressionEdits],
3217
+ css: [],
3218
+ imports: [
3219
+ ...nativeStyleImports,
3220
+ {
3221
+ content: `\nconst ${nativeLocal} = require('react-native').${nativeExport};`,
3222
+ origin: input.element.component.span,
3223
+ },
3224
+ {
3225
+ content: `\nconst ${stableLocal} = require('@tamagui/core')._withStableStyle(${nativeLocal}, (_theme, expressions) => [${styleParts.join(', ')}], ${themedStyleKeys ? 'true' : 'false'}, false);`,
3226
+ origin: input.element.component.span,
3227
+ },
3228
+ ],
3229
+ flattened: true,
3230
+ }
3231
+ }
3232
+ const styleContent = `style: ${nativeStyleSource}`
3233
+ const tagEdits = [
3234
+ input.element.component.span,
3235
+ input.element.component.closingSpan,
3236
+ ]
3237
+ .filter((span): span is NonNullable<typeof span> => !!span)
3238
+ .map((span) => ({
3239
+ start: span.start,
3240
+ end: span.end,
3241
+ content: nativeLocal,
3242
+ origin: span,
3243
+ }))
3244
+ if (input.element.form !== 'jsx') {
3245
+ const propsEdits = compiledPropsEdits(
3246
+ input,
3247
+ styleEntries,
3248
+ styleContent,
3249
+ (entry) => spreadReplacement(input.element.form, entry)
3250
+ )
3251
+ if (!propsEdits) {
3252
+ return bailout(
3253
+ input,
3254
+ 'local/unsupported-target',
3255
+ `Compiled ${input.element.form} call has no editable props argument`
3256
+ )
3257
+ }
3258
+ return {
3259
+ ok: true,
3260
+ edits: [...tagEdits, ...propsEdits],
3261
+ css: [],
3262
+ imports: [
3263
+ ...nativeStyleImports,
3264
+ {
3265
+ content: `\nconst ${nativeLocal} = require('react-native').${nativeExport};`,
3266
+ origin: input.element.component.span,
3267
+ },
3268
+ ],
3269
+ flattened: true,
3270
+ }
3271
+ }
3272
+ if (styleEntries.length === 0) {
3273
+ return {
3274
+ ok: true,
3275
+ edits: [
3276
+ ...tagEdits,
3277
+ {
3278
+ start: input.element.component.span.end,
3279
+ end: input.element.component.span.end,
3280
+ content: ` style={${nativeStyleSource}}`,
3281
+ origin: input.element.component.span,
3282
+ },
3283
+ ],
3284
+ css: [],
3285
+ imports: [
3286
+ ...nativeStyleImports,
3287
+ {
3288
+ content: `\nconst ${nativeLocal} = require('react-native').${nativeExport};`,
3289
+ origin: input.element.component.span,
3290
+ },
3291
+ ],
3292
+ flattened: true,
3293
+ }
3294
+ }
3295
+ const [first, ...rest] = styleEntries
3296
+ const firstNonStyle = first ? spreadReplacement('jsx', first) : ''
3297
+ return {
3298
+ ok: true,
3299
+ edits: [
3300
+ ...tagEdits,
3301
+ ...webPropEdits,
3302
+ {
3303
+ start: first!.span.start,
3304
+ end: first!.span.end,
3305
+ content: [`style={${nativeStyleSource}}`, firstNonStyle]
3306
+ .filter(Boolean)
3307
+ .join(' '),
3308
+ origin: first!.span,
3309
+ },
3310
+ ...rest.map((entry) => ({
3311
+ start: entry.span.start,
3312
+ end: entry.span.end,
3313
+ content: spreadReplacement('jsx', entry),
3314
+ origin: entry.span,
3315
+ })),
3316
+ ],
3317
+ css: [],
3318
+ imports: [
3319
+ ...nativeStyleImports,
3320
+ {
3321
+ content: `\nconst ${nativeLocal} = require('react-native').${nativeExport};`,
3322
+ origin: input.element.component.span,
3323
+ },
3324
+ ],
3325
+ flattened: true,
3326
+ }
3327
+ }
3328
+ const artifacts = extractedStyleArtifacts(
3329
+ split,
3330
+ props,
3331
+ options.tamaguiConfig,
3332
+ !component.domTag,
3333
+ Boolean(component.staticConfig.styleFrontend)
3334
+ )
3335
+ const className = artifacts.className
3336
+ const rawInlineStyle = split.viewProps?.style
3337
+ const inlineStyle =
3338
+ staticObject(rawInlineStyle) && Object.keys(rawInlineStyle).length > 0
3339
+ ? (rawInlineStyle as Record<string, unknown>)
3340
+ : null
3341
+ if (rawInlineStyle && !inlineStyle && !isSerializableNativeStyle(rawInlineStyle)) {
3342
+ return bailout(
3343
+ input,
3344
+ 'local/unsupported-target',
3345
+ 'Web inline style output is not a static serializable value'
3346
+ )
3347
+ }
3348
+ const programCSS: string[] = []
3349
+ const programClassSources: string[] = []
3350
+ if (
3351
+ domStyleProgram?.kind === 'prop' &&
3352
+ domStyleProgram.value.kind === 'dom-style'
3353
+ ) {
3354
+ for (const item of domStyleProgram.value.items) {
3355
+ if (item.value.kind !== 'static' || !staticObject(item.value.value)) {
3356
+ return bailout(
3357
+ input,
3358
+ 'local/dynamic-style-value',
3359
+ 'Every style() handle in a style array must be statically evaluable',
3360
+ item.value.span
3361
+ )
3362
+ }
3363
+ const itemSplit = resolveSplitStyles(
3364
+ item.value.value,
3365
+ partialStaticConfig(component.staticConfig)
3366
+ )
3367
+ if (!itemSplit) {
3368
+ return bailout(
3369
+ input,
3370
+ 'local/style-resolution-failed',
3371
+ 'A style() handle could not be resolved',
3372
+ item.value.span
3373
+ )
3374
+ }
3375
+ const itemInline = itemSplit.viewProps?.style
3376
+ if (staticObject(itemInline) && Object.keys(itemInline).length > 0) {
3377
+ return bailout(
3378
+ input,
3379
+ 'local/unsupported-target',
3380
+ 'Conditional web style() handles must lower to CSS classes',
3381
+ item.value.span
3382
+ )
3383
+ }
3384
+ const itemArtifacts = extractedStyleArtifacts(
3385
+ itemSplit,
3386
+ item.value.value,
3387
+ options.tamaguiConfig,
3388
+ false
3389
+ )
3390
+ programCSS.push(...itemArtifacts.css)
3391
+ if (itemArtifacts.className) {
3392
+ const condition = item.condition
3393
+ ? input.source.slice(item.condition.start, item.condition.end)
3394
+ : null
3395
+ programClassSources.push(
3396
+ condition
3397
+ ? `(${condition}) && ${JSON.stringify(itemArtifacts.className)}`
3398
+ : JSON.stringify(itemArtifacts.className)
3399
+ )
3400
+ }
3401
+ }
3402
+ }
3403
+ // web per-branch conditional lowering: both branches resolve through the
3404
+ // full pipeline; classes shared by both branches stay static and each
3405
+ // branch's remainder becomes one ternary className segment. v3's web
3406
+ // font architecture makes this cheap: sizes are family-independent
3407
+ // variables, so a conditional fontFamily flips only its font_* class.
3408
+ const baseStaticClasses = new Set(className.split(' ').filter(Boolean))
3409
+ const staticClasses = new Set(baseStaticClasses)
3410
+ const webConditionalCSS: string[] = []
3411
+ const webConditionalKeys = new Set<string>()
3412
+ const webConditionalEntries = supportsWebConditionalClasses ? webClassEntries : []
3413
+ // inline style from conditional branches: a value every branch shares
3414
+ // hoists into the element's style, and a value that differs becomes one
3415
+ // conditional inline property. the css driver keeps transitioned
3416
+ // properties inline, so this is how a conditional className carrying
3417
+ // `transition-*` classes lowers
3418
+ let webInlineStyle: Record<string, unknown> | null = inlineStyle
3419
+ const conditionalInlineProperties: string[] = []
3420
+ for (const entry of webConditionalEntries) {
3421
+ const tree = entry.value.kind === 'conditional' ? entry.value.tree : null
3422
+ const domain = frontendClassDomain(entry, component)
3423
+ if (!tree && !domain) continue
3424
+ const leaves = tree
3425
+ ? collectLeaves(tree)
3426
+ : domain!.map((value) => ({ value, dependencies: [] }))
3427
+ const leafArtifactsMap = new Map<
3428
+ (typeof leaves)[number],
3429
+ { classes: string[]; css: string[] }
3430
+ >()
3431
+ const leafStyles = new Map<(typeof leaves)[number], Record<string, unknown>>()
3432
+ const entryConditionalKeys = new Set<string>()
3433
+
3434
+ for (const leaf of leaves) {
3435
+ const { branchProps, branchCompleteProps } = propsForConditional(
3436
+ entry,
3437
+ leaf.value
3438
+ )
3439
+ const branchSplit = resolveSplitStyles(
3440
+ branchCompleteProps,
3441
+ component.staticConfig,
3442
+ cssAnimationDriver,
3443
+ component.displayName
3444
+ )
3445
+ if (!branchSplit) {
3446
+ return bailout(
3447
+ input,
3448
+ 'local/dynamic-style-value',
3449
+ `Conditional ${entry.name} branch could not be resolved`,
3450
+ entry.value.span
3451
+ )
3452
+ }
3453
+ for (const viewPropsKey of new Set([
3454
+ ...Object.keys(branchSplit.viewProps ?? {}),
3455
+ ...Object.keys(split.viewProps ?? {}),
3456
+ ])) {
3457
+ if (viewPropsKey === 'className' || viewPropsKey === 'style') continue
3458
+ if (
3459
+ JSON.stringify(branchSplit.viewProps?.[viewPropsKey]) !==
3460
+ JSON.stringify((split.viewProps as any)?.[viewPropsKey])
3461
+ ) {
3462
+ return bailout(
3463
+ input,
3464
+ 'local/dynamic-style-value',
3465
+ `Conditional ${entry.name} branch changes ${viewPropsKey}, which conditional classes cannot express`,
3466
+ entry.value.span
3467
+ )
3468
+ }
3469
+ }
3470
+ const branchInline = branchSplit.viewProps?.style
3471
+ if (branchInline !== undefined && !staticObject(branchInline)) {
3472
+ return bailout(
3473
+ input,
3474
+ 'local/dynamic-style-value',
3475
+ `Conditional ${entry.name} branch style is not a static object`,
3476
+ entry.value.span
3477
+ )
3478
+ }
3479
+ leafStyles.set(
3480
+ leaf,
3481
+ staticObject(branchInline) ? (branchInline as Record<string, unknown>) : {}
3482
+ )
3483
+ const changedKeys = new Set<string>()
3484
+ for (const key of new Set([
3485
+ ...Object.keys(branchSplit.classNames ?? {}),
3486
+ ...Object.keys(split.classNames ?? {}),
3487
+ ])) {
3488
+ if (
3489
+ JSON.stringify(branchSplit.classNames?.[key]) !==
3490
+ JSON.stringify(split.classNames?.[key])
3491
+ ) {
3492
+ changedKeys.add(key)
3493
+ }
3494
+ }
3495
+ const branchStyle = branchSplit.viewProps?.style
3496
+ const baseStyle = split.viewProps?.style
3497
+ if (staticObject(branchStyle) || staticObject(baseStyle)) {
3498
+ for (const key of new Set([
3499
+ ...Object.keys(staticObject(branchStyle) ? branchStyle : {}),
3500
+ ...Object.keys(staticObject(baseStyle) ? baseStyle : {}),
3501
+ ])) {
3502
+ if (
3503
+ JSON.stringify(
3504
+ staticObject(branchStyle) ? branchStyle[key] : undefined
3505
+ ) !== JSON.stringify(staticObject(baseStyle) ? baseStyle[key] : undefined)
3506
+ ) {
3507
+ changedKeys.add(key)
3508
+ }
3509
+ }
3510
+ }
3511
+ for (const key of changedKeys) {
3512
+ if (webConditionalKeys.has(key)) {
3513
+ return bailout(
3514
+ input,
3515
+ 'local/dynamic-style-value',
3516
+ `Multiple conditionals contribute ${key}; their interaction cannot be resolved per-branch`,
3517
+ entry.value.span
3518
+ )
3519
+ }
3520
+ entryConditionalKeys.add(key)
3521
+ }
3522
+ if (conflictsWithInline(changedKeys, laneAOwners, laneAMemberOwners)) {
3523
+ return bailout(
3524
+ input,
3525
+ 'local/dynamic-style-value',
3526
+ `${entry.name} and an inline dynamic style both contribute a CSS property; their precedence cannot be resolved per-branch`,
3527
+ entry.value.span
3528
+ )
3529
+ }
3530
+ const branchArtifacts = extractedStyleArtifacts(
3531
+ branchSplit,
3532
+ branchProps,
3533
+ options.tamaguiConfig,
3534
+ !component.domTag,
3535
+ Boolean(component.staticConfig.styleFrontend)
3536
+ )
3537
+ leafArtifactsMap.set(leaf, {
3538
+ classes: branchArtifacts.className.split(' ').filter(Boolean),
3539
+ css: branchArtifacts.css,
3540
+ })
3541
+ }
3542
+ for (const key of entryConditionalKeys) webConditionalKeys.add(key)
3543
+
3544
+ const styleKeys = new Set<string>(Object.keys(webInlineStyle ?? {}))
3545
+ for (const leaf of leaves) {
3546
+ for (const key of Object.keys(leafStyles.get(leaf) ?? {})) styleKeys.add(key)
3547
+ }
3548
+ for (const key of styleKeys) {
3549
+ const serialized = leaves.map((leaf) =>
3550
+ JSON.stringify(leafStyles.get(leaf)?.[key])
3551
+ )
3552
+ if (serialized.every((value) => value === serialized[0])) {
3553
+ if (serialized[0] === JSON.stringify(webInlineStyle?.[key])) continue
3554
+ const next = { ...webInlineStyle }
3555
+ if (serialized[0] === undefined) delete next[key]
3556
+ else next[key] = leafStyles.get(leaves[0]!)![key]
3557
+ webInlineStyle = Object.keys(next).length > 0 ? next : null
3558
+ continue
3559
+ }
3560
+ if (!tree) {
3561
+ return bailout(
3562
+ input,
3563
+ 'local/dynamic-style-value',
3564
+ `Conditional ${entry.name} branch changes style, which a class table cannot express`,
3565
+ entry.value.span
3566
+ )
3567
+ }
3568
+ const serializeValue = (node: BranchDecisionNode): string => {
3569
+ if (node.kind === 'leaf')
3570
+ return String(JSON.stringify(leafStyles.get(node)?.[key]))
3571
+ const test = input.source.slice(node.test.start, node.test.end)
3572
+ return `(${test}) ? ${serializeValue(node.whenTrue)} : ${serializeValue(node.whenFalse)}`
3573
+ }
3574
+ conditionalInlineProperties.push(
3575
+ `${JSON.stringify(key)}: (${serializeValue(tree)})`
3576
+ )
3577
+ }
3578
+
3579
+ for (const artifacts of leafArtifactsMap.values()) {
3580
+ webConditionalCSS.push(...artifacts.css)
3581
+ }
3582
+
3583
+ const allLeafArtifacts = leaves.map((l) => leafArtifactsMap.get(l)!)
3584
+ const sharedInConditional = new Set(
3585
+ allLeafArtifacts[0]?.classes.filter((c) =>
3586
+ allLeafArtifacts.every((a) => a.classes.includes(c))
3587
+ ) ?? []
3588
+ )
3589
+ for (const cls of baseStaticClasses) {
3590
+ if (!sharedInConditional.has(cls)) staticClasses.delete(cls)
3591
+ }
3592
+ for (const cls of sharedInConditional) {
3593
+ if (!baseStaticClasses.has(cls)) staticClasses.add(cls)
3594
+ }
3595
+
3596
+ const leafClasses = (leaf: (typeof leaves)[number]): string => {
3597
+ const artifacts = leafArtifactsMap.get(leaf)
3598
+ const only = artifacts
3599
+ ? artifacts.classes.filter((item) => !sharedInConditional.has(item))
3600
+ : []
3601
+ return only.join(' ')
3602
+ }
3603
+ function serializeWebTree(node: BranchDecisionNode): string {
3604
+ if (node.kind === 'leaf') return JSON.stringify(leafClasses(node))
3605
+ const test = input.source.slice(node.test.start, node.test.end)
3606
+ const truePart = serializeWebTree(node.whenTrue)
3607
+ const falsePart = serializeWebTree(node.whenFalse)
3608
+ return `(${test}) ? ${truePart} : ${falsePart}`
3609
+ }
3610
+
3611
+ if (leaves.every((leaf) => leafClasses(leaf) === '')) {
3612
+ // every branch's classes hoisted, or the branches differ only in
3613
+ // inline style
3614
+ } else if (tree) {
3615
+ programClassSources.push(serializeWebTree(tree))
3616
+ } else {
3617
+ // the runtime string picks its resolved classes; a string outside the
3618
+ // domain cannot occur, since the domain is the array's full contents
3619
+ const table = Object.fromEntries(
3620
+ leaves.map((leaf) => [leaf.value, leafClasses(leaf)])
3621
+ )
3622
+ const key = input.source.slice(entry.value.span.start, entry.value.span.end)
3623
+ programClassSources.push(`(${JSON.stringify(table)})[${key}]`)
3624
+ }
3625
+ }
3626
+ const webClassName = [...staticClasses].join(' ')
3627
+ const hasStyleProgram = programClassSources.length > 0
3628
+ const classNameExpression = hasStyleProgram
3629
+ ? `[${[JSON.stringify(webClassName), ...programClassSources].join(', ')}].filter(Boolean).join(" ")`
3630
+ : null
3631
+ const webInlineProperties = [
3632
+ ...(dynamicHostStyleProperties ?? []),
3633
+ ...conditionalInlineProperties,
3634
+ ]
3635
+ const serializedInlineStyle = serializedStyle(webInlineStyle, webInlineProperties)
3636
+ const jsxWebStyle = classNameExpression
3637
+ ? [
3638
+ `className={${classNameExpression}}`,
3639
+ serializedInlineStyle ? `style={${serializedInlineStyle}}` : '',
3640
+ ]
3641
+ .filter(Boolean)
3642
+ .join(' ')
3643
+ : jsxStyleAttributes(webClassName, webInlineStyle, webInlineProperties)
3644
+ const objectWebStyle = classNameExpression
3645
+ ? [
3646
+ `className: ${classNameExpression}`,
3647
+ serializedInlineStyle ? `style: ${serializedInlineStyle}` : '',
3648
+ ]
3649
+ .filter(Boolean)
3650
+ .join(', ')
3651
+ : objectStyleProperties(webClassName, webInlineStyle, webInlineProperties)
3652
+ const webCSS = [...new Set([...artifacts.css, ...programCSS, ...webConditionalCSS])]
3653
+ const webExtraProps = serializedProps(input.element.form, webDOMResult.additions)
3654
+ if (input.element.form !== 'jsx') {
3655
+ const replacement = [objectWebStyle, webExtraProps].filter(Boolean).join(', ')
3656
+ const propsEdits = compiledPropsEdits(input, styleEntries, replacement, (entry) =>
3657
+ spreadReplacement(input.element.form, entry)
3658
+ )
3659
+ if (!propsEdits) {
3660
+ return bailout(
3661
+ input,
3662
+ 'local/unsupported-target',
3663
+ `Compiled ${input.element.form} call has no editable props argument`
3664
+ )
3665
+ }
3666
+ return {
3667
+ ok: true,
3668
+ edits: [...tagEdits, ...webPropEdits, ...propsEdits],
3669
+ css: webCSS,
3670
+ imports: [],
3671
+ flattened: true,
3672
+ }
3673
+ }
3674
+ if (styleEntries.length === 0) {
3675
+ const attributes = [jsxWebStyle, webExtraProps].filter(Boolean).join(' ')
3676
+ return {
3677
+ ok: true,
3678
+ edits: attributes
3679
+ ? [
3680
+ ...tagEdits,
3681
+ ...webPropEdits,
3682
+ {
3683
+ start: input.element.component.span.end,
3684
+ end: input.element.component.span.end,
3685
+ content: ` ${attributes}`,
3686
+ origin: input.element.component.span,
3687
+ },
3688
+ ]
3689
+ : [...tagEdits, ...webPropEdits],
3690
+ css: webCSS,
3691
+ imports: [],
3692
+ flattened: true,
3693
+ }
3694
+ }
3695
+
3696
+ const [first, ...rest] = styleEntries
3697
+ const firstNonStyle = first ? spreadReplacement('jsx', first) : ''
3698
+ const attributes = [jsxWebStyle, webExtraProps, firstNonStyle]
3699
+ .filter(Boolean)
3700
+ .join(' ')
3701
+ return {
3702
+ ok: true,
3703
+ edits: [
3704
+ ...tagEdits,
3705
+ ...webPropEdits,
3706
+ {
3707
+ start: first!.span.start,
3708
+ end: first!.span.end,
3709
+ content: attributes,
3710
+ origin: first!.span,
3711
+ },
3712
+ ...rest.map((entry) => ({
3713
+ start: entry.span.start,
3714
+ end: entry.span.end,
3715
+ content: spreadReplacement('jsx', entry),
3716
+ origin: entry.span,
3717
+ })),
3718
+ ],
3719
+ css: webCSS,
3720
+ imports: [],
3721
+ flattened: true,
3722
+ }
3723
+ },
3724
+ }
3725
+ }
3726
+
3727
+ function bailout(
3728
+ input: LoweringCandidateInput,
3729
+ code:
3730
+ | 'local/unsupported-target'
3731
+ | 'local/dynamic-style-value'
3732
+ | 'local/unsafe-style-spread'
3733
+ | 'local/style-resolution-failed'
3734
+ | 'local/unsupported-child',
3735
+ message: string,
3736
+ span = input.element.span,
3737
+ zero?: { rule: ZeroRule; message?: string },
3738
+ prop?: string
3739
+ ): LoweringCandidateResult {
3740
+ return {
3741
+ ok: false,
3742
+ bailout: {
3743
+ code,
3744
+ kind: 'local',
3745
+ message,
3746
+ span,
3747
+ component: input.element.component.name,
3748
+ ...(prop && { prop }),
3749
+ ...(zero && { zeroRule: zero.rule, zeroMessage: zero.message }),
3750
+ },
3751
+ }
3752
+ }