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