@tamagui/static 3.0.0-beta.765.1 → 3.0.0-beta.804.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.
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ BranchDecisionNode,
2
3
  CompilerLoweringHost,
3
4
  CompilerTarget,
4
5
  LoweringCandidateInput,
@@ -9,7 +10,11 @@ import type {
9
10
  SourceEdit,
10
11
  ZeroRule,
11
12
  } from '@tamagui/compiler-core'
12
- import { zeroRuleMessage, zeroThemeBoundaryMessage } from '@tamagui/compiler-core'
13
+ import {
14
+ collectLeaves,
15
+ zeroRuleMessage,
16
+ zeroThemeBoundaryMessage,
17
+ } from '@tamagui/compiler-core'
13
18
  import {
14
19
  StyleObjectIdentifier,
15
20
  StyleObjectProperty,
@@ -267,6 +272,29 @@ function extractedStyleArtifacts(
267
272
  }
268
273
  }
269
274
 
275
+ function spreadNonStyleReplacement(
276
+ form: MaterializedElement['form'],
277
+ entry: MaterializedElement['entries'][number],
278
+ isPropIgnored: (name: string) => boolean,
279
+ rewriteProp: (name: string, value: unknown) => [string, unknown]
280
+ ): string {
281
+ if (
282
+ entry.kind !== 'spread' ||
283
+ entry.value.kind !== 'static' ||
284
+ !staticObject(entry.value.value)
285
+ ) {
286
+ return ''
287
+ }
288
+ const nonStyleEntries = Object.entries(entry.value.value)
289
+ .filter(([key]) => !isPropIgnored(key))
290
+ .map(([key, value]) => rewriteProp(key, value))
291
+ if (nonStyleEntries.length === 0) return ''
292
+ const objectSource = `{ ${nonStyleEntries
293
+ .map(([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value)}`)
294
+ .join(', ')} }`
295
+ return form === 'jsx' ? `{...${objectSource}}` : `...${objectSource}`
296
+ }
297
+
270
298
  /**
271
299
  * Granular edits inside a compiled-call props object. Whole-span replacement would
272
300
  * also cover the children property, so nested candidates could never both commit.
@@ -274,7 +302,8 @@ function extractedStyleArtifacts(
274
302
  function compiledPropsEdits(
275
303
  input: LoweringCandidateInput,
276
304
  styleEntries: MaterializedElement['entries'],
277
- replacement: string
305
+ replacement: string,
306
+ spreadReplacement?: (entry: MaterializedElement['entries'][number]) => string
278
307
  ): SourceEdit[] | null {
279
308
  const propsSpan = input.element.propsSpan
280
309
  if (!propsSpan) return null
@@ -309,8 +338,10 @@ function compiledPropsEdits(
309
338
  for (const [index, entry] of styleEntries.entries()) {
310
339
  let start = entry.span.start
311
340
  let end = entry.span.end
341
+ const nonStyle = spreadReplacement?.(entry) ?? ''
312
342
  if (index === 0) {
313
- edits.push({ start, end, content: replacement, origin: entry.span })
343
+ const content = [replacement, nonStyle].filter(Boolean).join(', ')
344
+ edits.push({ start, end, content, origin: entry.span })
314
345
  continue
315
346
  }
316
347
 
@@ -323,7 +354,7 @@ function compiledPropsEdits(
323
354
  while (cursor > 0 && /\s/.test(original[cursor]!)) cursor--
324
355
  if (original[cursor] === ',') start = propsSpan.start + cursor
325
356
  }
326
- edits.push({ start, end, content: '', origin: entry.span })
357
+ edits.push({ start, end, content: nonStyle, origin: entry.span })
327
358
  }
328
359
 
329
360
  const merged: SourceEdit[] = []
@@ -1251,6 +1282,7 @@ export function createTamaguiCompilerHost(
1251
1282
  }
1252
1283
  process.env.TAMAGUI_TARGET = platform
1253
1284
  try {
1285
+ core.prepareStyleStaticConfig(staticConfig)
1254
1286
  return core.getSplitStyles(
1255
1287
  props,
1256
1288
  staticConfig,
@@ -1297,11 +1329,7 @@ export function createTamaguiCompilerHost(
1297
1329
  if (!split) return null
1298
1330
 
1299
1331
  const inlineStyle = split.viewProps?.style
1300
- if (
1301
- staticObject(inlineStyle) &&
1302
- !inlineStyle['$$css'] &&
1303
- Object.keys(inlineStyle).length > 0
1304
- ) {
1332
+ if (staticObject(inlineStyle) && Object.keys(inlineStyle).length > 0) {
1305
1333
  return null
1306
1334
  }
1307
1335
 
@@ -1643,8 +1671,11 @@ export function createTamaguiCompilerHost(
1643
1671
  const namedAnimationDriver =
1644
1672
  animatedBy === null ? null : options.tamaguiConfig.animationDrivers?.[animatedBy]
1645
1673
  const namedCssAnimationDriver =
1646
- platform === 'web' && namedAnimationDriver?.outputStyle === 'css'
1647
- ? namedAnimationDriver
1674
+ platform === 'web' &&
1675
+ namedAnimationDriver &&
1676
+ !namedAnimationDriver.isStub &&
1677
+ namedAnimationDriver.outputStyle === 'css'
1678
+ ? (namedAnimationDriver as AnimationDriver)
1648
1679
  : null
1649
1680
  const cssAnimationDriver =
1650
1681
  namedCssAnimationDriver ??
@@ -1691,7 +1722,9 @@ export function createTamaguiCompilerHost(
1691
1722
  !options.disablePartialExtraction &&
1692
1723
  (input.element.form === 'jsx' || input.element.propsSpan !== null) &&
1693
1724
  dynamicStyleEntries.length > 0 &&
1694
- !input.element.entries.some((entry) => entry.kind === 'spread')
1725
+ !input.element.entries.some(
1726
+ (entry) => entry.kind === 'spread' && entry.value.kind !== 'static'
1727
+ )
1695
1728
  ) {
1696
1729
  const seen = new Set<string>()
1697
1730
  const properties: string[] = []
@@ -1784,15 +1817,12 @@ export function createTamaguiCompilerHost(
1784
1817
  // web per-branch conditional classes need the same preconditions the
1785
1818
  // native per-branch path has, and take precedence over web partial
1786
1819
  // extraction because a full flatten beats a runtime component
1787
- // exactly one conditional: several would need a shared class
1788
- // intersection across every branch combination, so elements with more
1789
- // keep the web partial-extraction path they had before
1790
1820
  const supportsWebConditionalClasses =
1791
1821
  platform === 'web' &&
1792
1822
  !options.disablePartialExtraction &&
1793
1823
  (input.element.form === 'jsx' || input.element.propsSpan !== null) &&
1794
1824
  dynamicHostStyleProperties === null &&
1795
- dynamicStyleEntries.length === 1 &&
1825
+ dynamicStyleEntries.length > 0 &&
1796
1826
  dynamicStyleEntries.every(
1797
1827
  (entry) =>
1798
1828
  entry.kind === 'prop' &&
@@ -1827,8 +1857,8 @@ export function createTamaguiCompilerHost(
1827
1857
  asChildEntry?.span
1828
1858
  )
1829
1859
  }
1830
- // group and the container props publish the same context: group writes
1831
- // the group name, container writes '@' and '@name'. descendants read it
1860
+ // group and container props publish separate keys in the same context:
1861
+ // group writes the group name, container writes '@' and '@name'. descendants read it
1832
1862
  // for `group-name-*` and `@name-*` clauses, so a flattened provider
1833
1863
  // silently breaks every consumer under it. web compiles these to CSS
1834
1864
  // container rules instead, so only native needs the bail.
@@ -1842,7 +1872,7 @@ export function createTamaguiCompilerHost(
1842
1872
  return bailout(
1843
1873
  input,
1844
1874
  'local/unsupported-target',
1845
- 'Native group containers remain on the runtime path'
1875
+ 'Native group and container providers remain on the runtime path'
1846
1876
  )
1847
1877
  }
1848
1878
  if (runtimeAnimationRequired && !cssAnimationDriver) {
@@ -1866,7 +1896,9 @@ export function createTamaguiCompilerHost(
1866
1896
  !supportsWebConditionalClasses &&
1867
1897
  component.partialRuntimeSafe
1868
1898
  ) {
1869
- const hasSpread = input.element.entries.some((entry) => entry.kind === 'spread')
1899
+ const hasSpread = input.element.entries.some(
1900
+ (entry) => entry.kind === 'spread' && entry.value.kind !== 'static'
1901
+ )
1870
1902
  const unsupportedRuntimeStyle = input.element.entries.find(
1871
1903
  (entry) =>
1872
1904
  entry.kind === 'prop' &&
@@ -1922,7 +1954,6 @@ export function createTamaguiCompilerHost(
1922
1954
  const partialInlineStyle = partialSplit?.viewProps?.style
1923
1955
  const hasPartialInlineStyle =
1924
1956
  staticObject(partialInlineStyle) &&
1925
- !partialInlineStyle['$$css'] &&
1926
1957
  Object.keys(partialInlineStyle).length > 0
1927
1958
  if (partialSplit && !hasPartialInlineStyle) {
1928
1959
  const artifacts = extractedStyleArtifacts(
@@ -2029,6 +2060,45 @@ export function createTamaguiCompilerHost(
2029
2060
  props
2030
2061
  )
2031
2062
  }
2063
+ const propsForConditional = (
2064
+ target: Extract<MaterializedElement['entries'][number], { kind: 'prop' }>,
2065
+ value: unknown
2066
+ ) => {
2067
+ const branchProps: Record<string, unknown> = {}
2068
+ for (const entry of input.element.entries) {
2069
+ if (entry === target) {
2070
+ branchProps[target.name] = value
2071
+ continue
2072
+ }
2073
+ if (entry.kind === 'child' || entry.value.kind !== 'static') continue
2074
+ if (entry.kind === 'spread') {
2075
+ if (staticObject(entry.value.value))
2076
+ Object.assign(branchProps, entry.value.value)
2077
+ } else {
2078
+ branchProps[entry.name] = entry.value.value
2079
+ }
2080
+ }
2081
+ if (component.domTag && branchProps.hidden) branchProps.display = 'none'
2082
+ if (resolvedCssTransition !== null) {
2083
+ branchProps.transition = resolvedCssTransition
2084
+ }
2085
+ if (component.domTag && platform === 'native') {
2086
+ for (const [name, styleKey] of DOM_STYLE_ATTRIBUTES) {
2087
+ if (name in branchProps) {
2088
+ branchProps[styleKey] = branchProps[name]
2089
+ delete branchProps[name]
2090
+ }
2091
+ }
2092
+ }
2093
+ const branchCompleteProps =
2094
+ platform === 'native' && component.domTag && branchProps.display === 'flex'
2095
+ ? core.mergeProps(
2096
+ core.mergeProps(defaultProps, NATIVE_FLEX_DEFAULTS),
2097
+ branchProps
2098
+ )
2099
+ : core.mergeProps(defaultProps, branchProps)
2100
+ return { branchProps, branchCompleteProps }
2101
+ }
2032
2102
  // Against completeProps, not props: clauses also arrive from styled()
2033
2103
  // defaults. Native resolution evaluates them against the build machine's
2034
2104
  // current state, so folding would freeze that state into the bundle.
@@ -2130,6 +2200,22 @@ export function createTamaguiCompilerHost(
2130
2200
  origin: span,
2131
2201
  }))
2132
2202
 
2203
+ const isPropIgnored = (name: string) =>
2204
+ isStyleProp(name, component) || isInvalidHostStyleProp(name, component)
2205
+ const spreadReplacement = (
2206
+ form: MaterializedElement['form'],
2207
+ entry: MaterializedElement['entries'][number]
2208
+ ) =>
2209
+ spreadNonStyleReplacement(form, entry, isPropIgnored, (name, value) => {
2210
+ if (platform !== 'web') return [name, value]
2211
+ if (name === 'testID') return ['data-testid', value]
2212
+ if (component.domTag && name === 'for') return ['htmlFor', value]
2213
+ if (component.domTag && name === 'role' && value === 'none') {
2214
+ return ['role', 'presentation']
2215
+ }
2216
+ return [name, value]
2217
+ })
2218
+
2133
2219
  let styleEntries = input.element.entries.filter(
2134
2220
  (entry) =>
2135
2221
  (entry.kind === 'prop' &&
@@ -2138,7 +2224,7 @@ export function createTamaguiCompilerHost(
2138
2224
  (entry.kind === 'spread' &&
2139
2225
  entry.value.kind === 'static' &&
2140
2226
  staticObject(entry.value.value) &&
2141
- Object.keys(entry.value.value).every(
2227
+ Object.keys(entry.value.value).some(
2142
2228
  (name) =>
2143
2229
  isStyleProp(name, component) || isInvalidHostStyleProp(name, component)
2144
2230
  ))
@@ -2176,6 +2262,23 @@ export function createTamaguiCompilerHost(
2176
2262
  invalidHostStyle.entry.span
2177
2263
  )
2178
2264
  }
2265
+ if (platform === 'native' && component.domTag) {
2266
+ const mixedSpread = styleEntries.find(
2267
+ (entry) =>
2268
+ entry.kind === 'spread' &&
2269
+ entry.value.kind === 'static' &&
2270
+ staticObject(entry.value.value) &&
2271
+ Object.keys(entry.value.value).some((name) => !isPropIgnored(name))
2272
+ )
2273
+ if (mixedSpread) {
2274
+ return bailout(
2275
+ input,
2276
+ 'local/unsafe-style-spread',
2277
+ 'Native DOM prop mapping requires non-style spread props to remain on the runtime path',
2278
+ mixedSpread.span
2279
+ )
2280
+ }
2281
+ }
2179
2282
  const webPropEdits: SourceEdit[] =
2180
2283
  platform === 'web'
2181
2284
  ? input.element.entries.flatMap((entry) => {
@@ -2203,22 +2306,6 @@ export function createTamaguiCompilerHost(
2203
2306
  ? webDOMProps(input, component.domTag)
2204
2307
  : { edits: [] as SourceEdit[], additions: [] as [string, string][] }
2205
2308
  webPropEdits.push(...webDOMResult.edits)
2206
- const unsafeSpread = input.element.entries.find(
2207
- (entry) =>
2208
- entry.kind === 'spread' &&
2209
- !styleEntries.includes(entry) &&
2210
- entry.value.kind === 'static' &&
2211
- staticObject(entry.value.value) &&
2212
- Object.keys(entry.value.value).some((name) => isStyleProp(name, component))
2213
- )
2214
- if (unsafeSpread) {
2215
- return bailout(
2216
- input,
2217
- 'local/unsafe-style-spread',
2218
- 'A mixed style/non-style spread cannot be removed transactionally',
2219
- unsafeSpread.span
2220
- )
2221
- }
2222
2309
 
2223
2310
  if (platform === 'native') {
2224
2311
  const nativeStyleResolved = split.viewProps?.style
@@ -2498,6 +2585,8 @@ export function createTamaguiCompilerHost(
2498
2585
  })
2499
2586
  }
2500
2587
  }
2588
+ const [first, ...rest] = styleEntries
2589
+ const firstNonStyle = first ? spreadReplacement('jsx', first) : ''
2501
2590
  const propsEdits =
2502
2591
  input.element.form === 'jsx'
2503
2592
  ? styleEntries.length === 0
@@ -2511,19 +2600,21 @@ export function createTamaguiCompilerHost(
2511
2600
  ]
2512
2601
  : [
2513
2602
  {
2514
- start: styleEntries[0]!.span.start,
2515
- end: styleEntries[0]!.span.end,
2516
- content: propertyContent,
2517
- origin: styleEntries[0]!.span,
2603
+ start: first!.span.start,
2604
+ end: first!.span.end,
2605
+ content: [propertyContent, firstNonStyle].filter(Boolean).join(' '),
2606
+ origin: first!.span,
2518
2607
  },
2519
- ...styleEntries.slice(1).map((entry) => ({
2608
+ ...rest.map((entry) => ({
2520
2609
  start: entry.span.start,
2521
2610
  end: entry.span.end,
2522
- content: '',
2611
+ content: spreadReplacement('jsx', entry),
2523
2612
  origin: entry.span,
2524
2613
  })),
2525
2614
  ]
2526
- : compiledPropsEdits(input, styleEntries, propertyContent)
2615
+ : compiledPropsEdits(input, styleEntries, propertyContent, (entry) =>
2616
+ spreadReplacement(input.element.form, entry)
2617
+ )
2527
2618
  if (!propsEdits) {
2528
2619
  return bailout(
2529
2620
  input,
@@ -2654,15 +2745,21 @@ export function createTamaguiCompilerHost(
2654
2745
  entry.value.kind === 'conditional' &&
2655
2746
  directStyleName(entry.name, component) !== 'opacity'
2656
2747
  ) {
2657
- const branchDiffs: Record<string, unknown>[] = []
2658
- for (const branchValue of [entry.value.whenTrue, entry.value.whenFalse]) {
2748
+ const tree = entry.value.tree
2749
+ const leaves = collectLeaves(tree)
2750
+ const leafDiffs = new Map<
2751
+ (typeof leaves)[number],
2752
+ Record<string, unknown>
2753
+ >()
2754
+ for (const leaf of leaves) {
2755
+ const { branchCompleteProps } = propsForConditional(entry, leaf.value)
2659
2756
  const branchSplit = resolveSplitStyles(
2660
- { ...completeProps, [entry.name]: branchValue },
2757
+ branchCompleteProps,
2661
2758
  component.staticConfig,
2662
2759
  cssAnimationDriver,
2663
2760
  component.displayName
2664
2761
  )
2665
- const branchStyle = branchSplit?.viewProps?.style
2762
+ const branchStyle = branchSplit?.viewProps?.style ?? {}
2666
2763
  if (!staticObject(branchStyle)) {
2667
2764
  return bailout(
2668
2765
  input,
@@ -2728,18 +2825,23 @@ export function createTamaguiCompilerHost(
2728
2825
  )
2729
2826
  }
2730
2827
  }
2731
- branchDiffs.push(diff)
2828
+ leafDiffs.set(leaf, diff)
2732
2829
  }
2733
- for (const diff of branchDiffs) {
2830
+ for (const diff of leafDiffs.values()) {
2734
2831
  for (const key of Object.keys(diff)) conditionalKeys.add(key)
2735
2832
  }
2736
- const index = expressions.length
2737
- expressions.push(
2738
- input.source.slice(entry.value.test.start, entry.value.test.end)
2739
- )
2740
- conditionalParts.push(
2741
- `expressions[${index}] ? ${JSON.stringify(branchDiffs[0])} : ${JSON.stringify(branchDiffs[1])}`
2742
- )
2833
+
2834
+ function serializeNativeTree(node: BranchDecisionNode): string {
2835
+ if (node.kind === 'leaf') {
2836
+ const diff = leafDiffs.get(node) ?? {}
2837
+ return JSON.stringify(diff)
2838
+ }
2839
+ const index = expressions.length
2840
+ expressions.push(input.source.slice(node.test.start, node.test.end))
2841
+ return `expressions[${index}] ? ${serializeNativeTree(node.whenTrue)} : ${serializeNativeTree(node.whenFalse)}`
2842
+ }
2843
+
2844
+ conditionalParts.push(serializeNativeTree(tree))
2743
2845
  } else {
2744
2846
  const index = expressions.length
2745
2847
  expressions.push(
@@ -2780,6 +2882,7 @@ export function createTamaguiCompilerHost(
2780
2882
  origin: span,
2781
2883
  }))
2782
2884
  const [first, ...rest] = styleEntries
2885
+ const firstNonStyle = first ? spreadReplacement('jsx', first) : ''
2783
2886
  const expressionEdits =
2784
2887
  styleEntries.length === 0
2785
2888
  ? []
@@ -2788,23 +2891,28 @@ export function createTamaguiCompilerHost(
2788
2891
  {
2789
2892
  start: first!.span.start,
2790
2893
  end: first!.span.end,
2791
- content:
2894
+ content: [
2792
2895
  expressions.length > 0
2793
2896
  ? `_expressions={[${expressions.join(', ')}]}`
2794
2897
  : '',
2898
+ firstNonStyle,
2899
+ ]
2900
+ .filter(Boolean)
2901
+ .join(' '),
2795
2902
  origin: first!.span,
2796
2903
  },
2797
2904
  ...rest.map((entry) => ({
2798
2905
  start: entry.span.start,
2799
2906
  end: entry.span.end,
2800
- content: '',
2907
+ content: spreadReplacement('jsx', entry),
2801
2908
  origin: entry.span,
2802
2909
  })),
2803
2910
  ]
2804
2911
  : compiledPropsEdits(
2805
2912
  input,
2806
2913
  styleEntries,
2807
- `_expressions: [${expressions.join(', ')}]`
2914
+ `_expressions: [${expressions.join(', ')}]`,
2915
+ (entry) => spreadReplacement(input.element.form, entry)
2808
2916
  )
2809
2917
  if (!expressionEdits) {
2810
2918
  return bailout(
@@ -2844,7 +2952,12 @@ export function createTamaguiCompilerHost(
2844
2952
  origin: span,
2845
2953
  }))
2846
2954
  if (input.element.form !== 'jsx') {
2847
- const propsEdits = compiledPropsEdits(input, styleEntries, styleContent)
2955
+ const propsEdits = compiledPropsEdits(
2956
+ input,
2957
+ styleEntries,
2958
+ styleContent,
2959
+ (entry) => spreadReplacement(input.element.form, entry)
2960
+ )
2848
2961
  if (!propsEdits) {
2849
2962
  return bailout(
2850
2963
  input,
@@ -2890,6 +3003,7 @@ export function createTamaguiCompilerHost(
2890
3003
  }
2891
3004
  }
2892
3005
  const [first, ...rest] = styleEntries
3006
+ const firstNonStyle = first ? spreadReplacement('jsx', first) : ''
2893
3007
  return {
2894
3008
  ok: true,
2895
3009
  edits: [
@@ -2898,13 +3012,15 @@ export function createTamaguiCompilerHost(
2898
3012
  {
2899
3013
  start: first!.span.start,
2900
3014
  end: first!.span.end,
2901
- content: `style={${nativeStyleSource}}`,
3015
+ content: [`style={${nativeStyleSource}}`, firstNonStyle]
3016
+ .filter(Boolean)
3017
+ .join(' '),
2902
3018
  origin: first!.span,
2903
3019
  },
2904
3020
  ...rest.map((entry) => ({
2905
3021
  start: entry.span.start,
2906
3022
  end: entry.span.end,
2907
- content: '',
3023
+ content: spreadReplacement('jsx', entry),
2908
3024
  origin: entry.span,
2909
3025
  })),
2910
3026
  ],
@@ -2929,9 +3045,7 @@ export function createTamaguiCompilerHost(
2929
3045
  const className = artifacts.className
2930
3046
  const rawInlineStyle = split.viewProps?.style
2931
3047
  const inlineStyle =
2932
- staticObject(rawInlineStyle) &&
2933
- !rawInlineStyle['$$css'] &&
2934
- Object.keys(rawInlineStyle).length > 0
3048
+ staticObject(rawInlineStyle) && Object.keys(rawInlineStyle).length > 0
2935
3049
  ? (rawInlineStyle as Record<string, unknown>)
2936
3050
  : null
2937
3051
  if (rawInlineStyle && !inlineStyle && !isSerializableNativeStyle(rawInlineStyle)) {
@@ -2969,11 +3083,7 @@ export function createTamaguiCompilerHost(
2969
3083
  )
2970
3084
  }
2971
3085
  const itemInline = itemSplit.viewProps?.style
2972
- if (
2973
- staticObject(itemInline) &&
2974
- !itemInline['$$css'] &&
2975
- Object.keys(itemInline).length > 0
2976
- ) {
3086
+ if (staticObject(itemInline) && Object.keys(itemInline).length > 0) {
2977
3087
  return bailout(
2978
3088
  input,
2979
3089
  'local/unsupported-target',
@@ -3005,17 +3115,30 @@ export function createTamaguiCompilerHost(
3005
3115
  // branch's remainder becomes one ternary className segment. v3's web
3006
3116
  // font architecture makes this cheap: sizes are family-independent
3007
3117
  // variables, so a conditional fontFamily flips only its font_* class.
3008
- let webClassName = className
3118
+ const baseStaticClasses = new Set(className.split(' ').filter(Boolean))
3119
+ const staticClasses = new Set(baseStaticClasses)
3009
3120
  const webConditionalCSS: string[] = []
3121
+ const webConditionalKeys = new Set<string>()
3010
3122
  const webConditionalEntries = supportsWebConditionalClasses
3011
3123
  ? dynamicStyleEntries.filter((entry) => entry.value.kind === 'conditional')
3012
3124
  : []
3013
3125
  for (const entry of webConditionalEntries) {
3014
3126
  if (entry.value.kind !== 'conditional') continue
3015
- const branches: { classes: string[]; css: string[] }[] = []
3016
- for (const branchValue of [entry.value.whenTrue, entry.value.whenFalse]) {
3127
+ const tree = entry.value.tree
3128
+ const leaves = collectLeaves(tree)
3129
+ const leafArtifactsMap = new Map<
3130
+ (typeof leaves)[number],
3131
+ { classes: string[]; css: string[] }
3132
+ >()
3133
+ const entryConditionalKeys = new Set<string>()
3134
+
3135
+ for (const leaf of leaves) {
3136
+ const { branchProps, branchCompleteProps } = propsForConditional(
3137
+ entry,
3138
+ leaf.value
3139
+ )
3017
3140
  const branchSplit = resolveSplitStyles(
3018
- { ...completeProps, [entry.name]: branchValue },
3141
+ branchCompleteProps,
3019
3142
  component.staticConfig,
3020
3143
  cssAnimationDriver,
3021
3144
  component.displayName
@@ -3045,33 +3168,94 @@ export function createTamaguiCompilerHost(
3045
3168
  )
3046
3169
  }
3047
3170
  }
3171
+ const changedKeys = new Set<string>()
3172
+ for (const key of new Set([
3173
+ ...Object.keys(branchSplit.classNames ?? {}),
3174
+ ...Object.keys(split.classNames ?? {}),
3175
+ ])) {
3176
+ if (
3177
+ JSON.stringify(branchSplit.classNames?.[key]) !==
3178
+ JSON.stringify(split.classNames?.[key])
3179
+ ) {
3180
+ changedKeys.add(key)
3181
+ }
3182
+ }
3183
+ const branchStyle = branchSplit.viewProps?.style
3184
+ const baseStyle = split.viewProps?.style
3185
+ if (staticObject(branchStyle) || staticObject(baseStyle)) {
3186
+ for (const key of new Set([
3187
+ ...Object.keys(staticObject(branchStyle) ? branchStyle : {}),
3188
+ ...Object.keys(staticObject(baseStyle) ? baseStyle : {}),
3189
+ ])) {
3190
+ if (
3191
+ JSON.stringify(
3192
+ staticObject(branchStyle) ? branchStyle[key] : undefined
3193
+ ) !== JSON.stringify(staticObject(baseStyle) ? baseStyle[key] : undefined)
3194
+ ) {
3195
+ changedKeys.add(key)
3196
+ }
3197
+ }
3198
+ }
3199
+ for (const key of changedKeys) {
3200
+ if (webConditionalKeys.has(key)) {
3201
+ return bailout(
3202
+ input,
3203
+ 'local/dynamic-style-value',
3204
+ `Multiple conditionals contribute ${key}; their interaction cannot be resolved per-branch`,
3205
+ entry.value.span
3206
+ )
3207
+ }
3208
+ entryConditionalKeys.add(key)
3209
+ }
3048
3210
  const branchArtifacts = extractedStyleArtifacts(
3049
3211
  branchSplit,
3050
- { ...props, [entry.name]: branchValue },
3212
+ branchProps,
3051
3213
  options.tamaguiConfig,
3052
3214
  !component.domTag,
3053
3215
  Boolean(component.staticConfig.styleFrontend)
3054
3216
  )
3055
- branches.push({
3217
+ leafArtifactsMap.set(leaf, {
3056
3218
  classes: branchArtifacts.className.split(' ').filter(Boolean),
3057
3219
  css: branchArtifacts.css,
3058
3220
  })
3059
3221
  }
3060
- const [whenTrue, whenFalse] = branches
3061
- const shared = new Set(
3062
- whenTrue!.classes.filter((item) => whenFalse!.classes.includes(item))
3222
+ for (const key of entryConditionalKeys) webConditionalKeys.add(key)
3223
+
3224
+ for (const artifacts of leafArtifactsMap.values()) {
3225
+ webConditionalCSS.push(...artifacts.css)
3226
+ }
3227
+
3228
+ const allLeafArtifacts = leaves.map((l) => leafArtifactsMap.get(l)!)
3229
+ const sharedInConditional = new Set(
3230
+ allLeafArtifacts[0]?.classes.filter((c) =>
3231
+ allLeafArtifacts.every((a) => a.classes.includes(c))
3232
+ ) ?? []
3063
3233
  )
3064
- const trueOnly = whenTrue!.classes.filter((item) => !shared.has(item))
3065
- const falseOnly = whenFalse!.classes.filter((item) => !shared.has(item))
3066
- webClassName = [...shared].join(' ')
3067
- webConditionalCSS.push(...whenTrue!.css, ...whenFalse!.css)
3068
- if (trueOnly.length > 0 || falseOnly.length > 0) {
3069
- const test = input.source.slice(entry.value.test.start, entry.value.test.end)
3070
- programClassSources.push(
3071
- `(${test}) ? ${JSON.stringify(trueOnly.join(' '))} : ${JSON.stringify(falseOnly.join(' '))}`
3072
- )
3234
+ for (const cls of baseStaticClasses) {
3235
+ if (!sharedInConditional.has(cls)) staticClasses.delete(cls)
3236
+ }
3237
+ for (const cls of sharedInConditional) {
3238
+ if (!baseStaticClasses.has(cls)) staticClasses.add(cls)
3073
3239
  }
3240
+
3241
+ function serializeWebTree(node: BranchDecisionNode): string {
3242
+ if (node.kind === 'leaf') {
3243
+ const artifacts = leafArtifactsMap.get(node)
3244
+ const only = artifacts
3245
+ ? artifacts.classes.filter((item) => !sharedInConditional.has(item))
3246
+ : []
3247
+ return JSON.stringify(only.join(' '))
3248
+ }
3249
+ const test = input.source.slice(node.test.start, node.test.end)
3250
+ const truePart = serializeWebTree(node.whenTrue)
3251
+ const falsePart = serializeWebTree(node.whenFalse)
3252
+ return `(${test}) ? ${truePart} : ${falsePart}`
3253
+ }
3254
+
3255
+ const classExpr = serializeWebTree(tree)
3256
+ programClassSources.push(classExpr)
3074
3257
  }
3258
+ const webClassName = [...staticClasses].join(' ')
3075
3259
  const hasStyleProgram = programClassSources.length > 0
3076
3260
  const classNameExpression = hasStyleProgram
3077
3261
  ? `[${[JSON.stringify(webClassName), ...programClassSources].join(', ')}].filter(Boolean).join(" ")`
@@ -3104,7 +3288,9 @@ export function createTamaguiCompilerHost(
3104
3288
  const webExtraProps = serializedProps(input.element.form, webDOMResult.additions)
3105
3289
  if (input.element.form !== 'jsx') {
3106
3290
  const replacement = [objectWebStyle, webExtraProps].filter(Boolean).join(', ')
3107
- const propsEdits = compiledPropsEdits(input, styleEntries, replacement)
3291
+ const propsEdits = compiledPropsEdits(input, styleEntries, replacement, (entry) =>
3292
+ spreadReplacement(input.element.form, entry)
3293
+ )
3108
3294
  if (!propsEdits) {
3109
3295
  return bailout(
3110
3296
  input,
@@ -3143,7 +3329,10 @@ export function createTamaguiCompilerHost(
3143
3329
  }
3144
3330
 
3145
3331
  const [first, ...rest] = styleEntries
3146
- const attributes = [jsxWebStyle, webExtraProps].filter(Boolean).join(' ')
3332
+ const firstNonStyle = first ? spreadReplacement('jsx', first) : ''
3333
+ const attributes = [jsxWebStyle, webExtraProps, firstNonStyle]
3334
+ .filter(Boolean)
3335
+ .join(' ')
3147
3336
  return {
3148
3337
  ok: true,
3149
3338
  edits: [
@@ -3158,7 +3347,7 @@ export function createTamaguiCompilerHost(
3158
3347
  ...rest.map((entry) => ({
3159
3348
  start: entry.span.start,
3160
3349
  end: entry.span.end,
3161
- content: '',
3350
+ content: spreadReplacement('jsx', entry),
3162
3351
  origin: entry.span,
3163
3352
  })),
3164
3353
  ],