@tamagui/static 3.0.0-beta.924.1 → 3.0.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 (59) hide show
  1. package/dist/cjs/compilerHost.cjs +241 -75
  2. package/dist/cjs/exports.cjs +5 -1
  3. package/dist/cjs/extractor/bundle.cjs +14 -9
  4. package/dist/cjs/extractor/bundleConfig.cjs +0 -1
  5. package/dist/cjs/extractor/esbuildTsconfigPaths.cjs +81 -45
  6. package/dist/cjs/extractor/generateConfigMarkdown.cjs +492 -0
  7. package/dist/cjs/extractor/loadTamagui.cjs +28 -20
  8. package/dist/cjs/extractor/regenerateConfig.cjs +30 -20
  9. package/dist/cjs/zero/islands.cjs +2 -1
  10. package/dist/cjs/zero/theme.cjs +6 -3
  11. package/dist/esm/compilerHost.mjs +242 -76
  12. package/dist/esm/compilerHost.mjs.map +1 -1
  13. package/dist/esm/exports.mjs +3 -1
  14. package/dist/esm/extractor/bundle.mjs +14 -9
  15. package/dist/esm/extractor/bundle.mjs.map +1 -1
  16. package/dist/esm/extractor/bundleConfig.mjs +0 -1
  17. package/dist/esm/extractor/bundleConfig.mjs.map +1 -1
  18. package/dist/esm/extractor/esbuildTsconfigPaths.mjs +80 -45
  19. package/dist/esm/extractor/esbuildTsconfigPaths.mjs.map +1 -1
  20. package/dist/esm/extractor/generateConfigMarkdown.mjs +472 -0
  21. package/dist/esm/extractor/generateConfigMarkdown.mjs.map +1 -0
  22. package/dist/esm/extractor/loadTamagui.mjs +28 -20
  23. package/dist/esm/extractor/loadTamagui.mjs.map +1 -1
  24. package/dist/esm/extractor/regenerateConfig.mjs +30 -20
  25. package/dist/esm/extractor/regenerateConfig.mjs.map +1 -1
  26. package/dist/esm/zero/islands.mjs +2 -1
  27. package/dist/esm/zero/islands.mjs.map +1 -1
  28. package/dist/esm/zero/theme.mjs +7 -4
  29. package/dist/esm/zero/theme.mjs.map +1 -1
  30. package/package.json +23 -20
  31. package/src/compilerHost.ts +387 -160
  32. package/src/exports.ts +3 -0
  33. package/src/extractor/bundle.ts +19 -7
  34. package/src/extractor/bundleConfig.ts +0 -1
  35. package/src/extractor/esbuildTsconfigPaths.ts +106 -78
  36. package/src/extractor/generateConfigMarkdown.ts +610 -0
  37. package/src/extractor/loadTamagui.ts +55 -24
  38. package/src/extractor/regenerateConfig.ts +36 -35
  39. package/src/zero/islands.ts +2 -1
  40. package/src/zero/options.ts +8 -3
  41. package/src/zero/theme.ts +29 -4
  42. package/types/compilerHost.d.ts.map +1 -1
  43. package/types/exports.d.ts +2 -0
  44. package/types/exports.d.ts.map +1 -1
  45. package/types/extractor/bundle.d.ts.map +1 -1
  46. package/types/extractor/bundleConfig.d.ts.map +1 -1
  47. package/types/extractor/esbuildTsconfigPaths.d.ts +5 -7
  48. package/types/extractor/esbuildTsconfigPaths.d.ts.map +1 -1
  49. package/types/extractor/generateConfigMarkdown.d.ts +5 -0
  50. package/types/extractor/generateConfigMarkdown.d.ts.map +1 -0
  51. package/types/extractor/loadTamagui.d.ts +13 -1
  52. package/types/extractor/loadTamagui.d.ts.map +1 -1
  53. package/types/extractor/regenerateConfig.d.ts.map +1 -1
  54. package/types/zero/identity.d.ts +1 -1
  55. package/types/zero/islands.d.ts.map +1 -1
  56. package/types/zero/options.d.ts +8 -3
  57. package/types/zero/options.d.ts.map +1 -1
  58. package/types/zero/theme.d.ts +2 -0
  59. package/types/zero/theme.d.ts.map +1 -1
@@ -2,6 +2,7 @@ import type {
2
2
  BranchDecisionNode,
3
3
  CompilerLoweringHost,
4
4
  CompilerTarget,
5
+ DynamicEvaluation,
5
6
  LoweringCandidateInput,
6
7
  LoweringCandidateResult,
7
8
  LoweringComponent,
@@ -27,7 +28,6 @@ import {
27
28
  } from '@tamagui/helpers'
28
29
  import {
29
30
  ATTRIBUTES,
30
- DISPLAY_WEB_RESET,
31
31
  EVENTS,
32
32
  NATIVE_BACKING,
33
33
  NATIVE_BLOCK_DEFAULTS,
@@ -39,6 +39,7 @@ import {
39
39
  TAG_WEB_DEFAULTS,
40
40
  type TagName,
41
41
  } from '@tamagui/dom'
42
+ import { resolveTransition, toCSSTransition } from '@tamagui/animation-helpers'
42
43
  import {
43
44
  createModifierRegistry,
44
45
  parseTransition,
@@ -139,14 +140,6 @@ function componentKey(resolvedId: string, exportName: string): string {
139
140
  return `${resolvedId}#${exportName}`
140
141
  }
141
142
 
142
- function cssFromRules(rules: Record<string, any>): string[] {
143
- return Object.values(rules).flatMap((styleObject: any) => {
144
- const identifier = styleObject?.[StyleObjectIdentifier]
145
- const styleRules = styleObject?.[StyleObjectRules]
146
- return identifier && Array.isArray(styleRules) ? styleRules : []
147
- })
148
- }
149
-
150
143
  function jsxClassName(value: string): string {
151
144
  return `className=${JSON.stringify(value)}`
152
145
  }
@@ -214,7 +207,16 @@ function extractedStyleArtifacts(
214
207
  ? viewClassName.slice(0, -callerClassName.length).trimEnd()
215
208
  : viewClassName
216
209
  const rules = split.rulesToInsert ?? {}
217
- const mediaNames = new Set(Object.keys(config.media ?? {}))
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
+ }
218
220
  const pseudoNames = [
219
221
  'hover',
220
222
  'press',
@@ -243,21 +245,14 @@ function extractedStyleArtifacts(
243
245
  ])
244
246
  for (const identifier of identifiers) {
245
247
  const key = classKeys.get(identifier) ?? ''
246
- const css = cssFromRules(
247
- Object.fromEntries(
248
- Object.entries(rules).filter(
249
- ([, styleObject]: [string, any]) =>
250
- styleObject?.[StyleObjectIdentifier] === identifier
251
- )
252
- )
253
- ).join('')
248
+ const css = (rulesByIdentifier.get(identifier) ?? []).join('')
254
249
  if (identifier.startsWith('t_group_')) {
255
250
  buckets.group.push(identifier)
256
251
  } else if (pseudoNames.some((name) => key.endsWith(`-${name}`))) {
257
252
  buckets.pseudo.push(identifier)
258
253
  } else if (css.includes('.t_')) {
259
254
  buckets.theme.push(identifier)
260
- } else if ([...mediaNames].some((name) => key.endsWith(`-${name}`))) {
255
+ } else if (mediaNames.some((name) => key.endsWith(`-${name}`))) {
261
256
  buckets.media.push(identifier)
262
257
  } else {
263
258
  buckets.normal.push(identifier)
@@ -278,14 +273,7 @@ function extractedStyleArtifacts(
278
273
  .join(' ')
279
274
  : ''
280
275
  const css = orderedIdentifiers.flatMap((identifier) => {
281
- const identifierRules = cssFromRules(
282
- Object.fromEntries(
283
- Object.entries(rules).filter(
284
- ([, styleObject]: [string, any]) =>
285
- styleObject?.[StyleObjectIdentifier] === identifier
286
- )
287
- )
288
- )
276
+ const identifierRules = rulesByIdentifier.get(identifier) ?? []
289
277
  // The legacy static theme-block path adds one root specificity level after
290
278
  // resolving tokens. Keep that output contract until the legacy oracle is removed.
291
279
  return buckets.theme.includes(identifier)
@@ -403,7 +391,6 @@ const compilerStyleProps = new Set([
403
391
  'group',
404
392
  'transition',
405
393
  'animation',
406
- 'animateOnly',
407
394
  'animatePresence',
408
395
  'animatedBy',
409
396
  'fontFamily',
@@ -413,7 +400,6 @@ const compilerStyleProps = new Set([
413
400
  const runtimeAnimationProps = new Set([
414
401
  'transition',
415
402
  'animation',
416
- 'animateOnly',
417
403
  'animatePresence',
418
404
  'animatedBy',
419
405
  ])
@@ -701,6 +687,61 @@ const nativePointerEventProps = new Set([
701
687
  'onPointerUp',
702
688
  ])
703
689
 
690
+ // expandStyle writes every shorthand as four longhands, and fabric pays per prop
691
+ // on mount (200 views: 8.8ms as longhands, 5.7ms as shorthands). when a hoisted
692
+ // static style holds all four sides equal and no other key of that family, the
693
+ // shorthand resolves identically in yoga, and any longhand in a later style
694
+ // object still overrides it
695
+ const nativeShorthandFamilies: [string, string[], RegExp][] = [
696
+ ['padding', ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'], /^padding/],
697
+ ['margin', ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'], /^margin/],
698
+ [
699
+ 'borderWidth',
700
+ ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'],
701
+ /^border.*Width$/,
702
+ ],
703
+ [
704
+ 'borderColor',
705
+ ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'],
706
+ /^border.*Color$/,
707
+ ],
708
+ [
709
+ 'borderRadius',
710
+ [
711
+ 'borderTopLeftRadius',
712
+ 'borderTopRightRadius',
713
+ 'borderBottomRightRadius',
714
+ 'borderBottomLeftRadius',
715
+ ],
716
+ /^border.*Radius$/,
717
+ ],
718
+ ]
719
+
720
+ function collapseNativeShorthands(style: unknown) {
721
+ if (!staticObject(style)) return style
722
+ const keys = Object.keys(style)
723
+ const collapsed = new Map<string, string>()
724
+ for (const [shorthand, sides, family] of nativeShorthandFamilies) {
725
+ const value = style[sides[0]]
726
+ if (
727
+ value !== undefined &&
728
+ !(shorthand in style) &&
729
+ sides.every((side) => style[side] === value) &&
730
+ keys.filter((key) => family.test(key)).length === sides.length
731
+ ) {
732
+ for (const side of sides) collapsed.set(side, shorthand)
733
+ }
734
+ }
735
+ if (!collapsed.size) return style
736
+ const out: Record<string, unknown> = {}
737
+ for (const key of keys) {
738
+ const shorthand = collapsed.get(key)
739
+ if (!shorthand) out[key] = style[key]
740
+ else if (!(shorthand in out)) out[shorthand] = style[key]
741
+ }
742
+ return out
743
+ }
744
+
704
745
  function isSerializableNativeStyle(value: unknown): boolean {
705
746
  if (value == null || typeof value === 'number' || typeof value === 'boolean') {
706
747
  return true
@@ -713,6 +754,61 @@ function isSerializableNativeStyle(value: unknown): boolean {
713
754
  return Object.values(value).every(isSerializableNativeStyle)
714
755
  }
715
756
 
757
+ /**
758
+ * native host styles contain only completed pixel metrics. this compact record
759
+ * retains the authored text metric long enough for a DOM primitive or runtime
760
+ * Text descendant to apply it against its effective font size.
761
+ */
762
+ type NativeTextMetrics = {
763
+ fontSize?: number
764
+ lineHeight?: number | `${number}px` | 'normal'
765
+ }
766
+
767
+ function nativeTextMetrics(result: unknown): NativeTextMetrics | null {
768
+ const value = (result as { nativeTextMetrics?: unknown } | null)?.nativeTextMetrics
769
+ if (!staticObject(value)) return null
770
+ const { fontSize, lineHeight } = value
771
+ if (
772
+ (fontSize !== undefined && typeof fontSize !== 'number') ||
773
+ (lineHeight !== undefined &&
774
+ typeof lineHeight !== 'number' &&
775
+ typeof lineHeight !== 'string')
776
+ ) {
777
+ return null
778
+ }
779
+ return fontSize === undefined && lineHeight === undefined
780
+ ? null
781
+ : (value as NativeTextMetrics)
782
+ }
783
+
784
+ function mergeNativeTextMetrics(
785
+ current: NativeTextMetrics | null,
786
+ next: NativeTextMetrics | null
787
+ ): NativeTextMetrics | null {
788
+ if (!current) return next
789
+ if (!next) return current
790
+ return {
791
+ fontSize: next.fontSize ?? current.fontSize,
792
+ lineHeight: next.lineHeight ?? current.lineHeight,
793
+ }
794
+ }
795
+
796
+ function hasChildThatMayRenderReactNode(element: MaterializedElement): boolean {
797
+ return element.entries.some((entry) => {
798
+ if (entry.kind !== 'child' || entry.value.kind === 'empty') return false
799
+ if (entry.value.kind === 'element') return true
800
+ if (entry.value.kind === 'bailout') return entry.value.dynamic === null
801
+ if (entry.value.kind === 'conditional') {
802
+ return collectLeaves(entry.value.tree).some(
803
+ ({ value }) =>
804
+ value === undefined || (value !== null && typeof value === 'object')
805
+ )
806
+ }
807
+ if (entry.value.kind !== 'static') return true
808
+ return entry.value.value !== null && typeof entry.value.value === 'object'
809
+ })
810
+ }
811
+
716
812
  function unusedIdentifier(source: string, base: string): string {
717
813
  let candidate = base
718
814
  let suffix = 0
@@ -1051,6 +1147,7 @@ export function createTamaguiCompilerHost(
1051
1147
  !options.tamaguiConfig.animationDrivers
1052
1148
  ? configuredAnimationDriver
1053
1149
  : null
1150
+ const nativeTextMetricsByElement = new WeakMap<MaterializedElement, NativeTextMetrics>()
1054
1151
  const resolveStaticCssTransition = (
1055
1152
  value: unknown,
1056
1153
  transitionPresets: Record<string, unknown>
@@ -1068,32 +1165,23 @@ export function createTamaguiCompilerHost(
1068
1165
  for (const payload of payloads) {
1069
1166
  const transition = parseTransition(payload, transitionPresetNames)
1070
1167
  if (!transition.ok) return null
1071
- if (
1072
- transition.value.kind === 'global' ||
1073
- transition.value.entries.every((entry) => entry.timing.type === 'css')
1074
- ) {
1168
+ if (transition.value.kind === 'global') {
1075
1169
  resolvedPayloads.push(payload)
1076
1170
  continue
1077
1171
  }
1078
- if (
1079
- transition.value.entries.length !== 1 ||
1080
- transition.value.entries[0]!.timing.type !== 'preset'
1081
- ) {
1082
- return null
1083
- }
1084
- const preset = transitionPresets[transition.value.entries[0]!.timing.name]
1085
- if (typeof preset !== 'string') return null
1086
- const parsedPreset = parseTransition(preset)
1087
- if (
1088
- !parsedPreset.ok ||
1089
- parsedPreset.value.kind !== 'transition' ||
1090
- parsedPreset.value.entries.length !== 1 ||
1091
- parsedPreset.value.entries[0]!.property !== 'all' ||
1092
- parsedPreset.value.entries[0]!.timing.type !== 'css'
1093
- ) {
1094
- return null
1172
+ // the runtime's own resolver, so a preset lowers to exactly the css the
1173
+ // css driver would have emitted. a preset of any shape works, springs
1174
+ // included: they become a `linear()` easing here rather than forcing the
1175
+ // element onto the runtime path.
1176
+ const resolved = resolveTransition(payload, { animations: transitionPresets })
1177
+ if (resolved.diagnostics.length > 0) return null
1178
+ if (!resolved.fused) {
1179
+ resolvedPayloads.push(payload)
1180
+ continue
1095
1181
  }
1096
- resolvedPayloads.push(`all ${preset}`)
1182
+ const css = toCSSTransition(resolved)
1183
+ if (css === undefined) return null
1184
+ resolvedPayloads.push(css)
1097
1185
  }
1098
1186
  let payloadIndex = 0
1099
1187
  const resolved: string[] = []
@@ -1171,10 +1259,13 @@ export function createTamaguiCompilerHost(
1171
1259
  | StaticConfig
1172
1260
  | undefined
1173
1261
  if (!base) return null
1262
+ // Web keeps only the semantic defaults as atomic defaults. Undoing the
1263
+ // browser stylesheet is the `:where(.is_DOM)` host reset, and the
1264
+ // extracted className below keeps the runtime base so compiled hosts
1265
+ // carry `is_DOM` for it to match.
1174
1266
  const platformDefaults =
1175
1267
  platform === 'web'
1176
1268
  ? {
1177
- ...DISPLAY_WEB_RESET[row.display],
1178
1269
  ...row.defaults,
1179
1270
  ...TAG_WEB_DEFAULTS[tag],
1180
1271
  }
@@ -1190,6 +1281,7 @@ export function createTamaguiCompilerHost(
1190
1281
  displayName: tag,
1191
1282
  staticConfig: normalizeStaticConfig({
1192
1283
  ...base,
1284
+ isDOM: true,
1193
1285
  isInput: row.backing === 'textinput',
1194
1286
  // CSS text properties may be authored on any element and inherited by
1195
1287
  // descendant text, including from a View-backed tag on native.
@@ -1284,8 +1376,11 @@ export function createTamaguiCompilerHost(
1284
1376
  const defaultProps = resolved.staticConfig.defaultProps ?? {}
1285
1377
  return {
1286
1378
  key: resolved.key,
1379
+ // a HOC (themed icons, createStyledHOC) renders a real component, so
1380
+ // flattening it to a host element drops everything it renders
1287
1381
  canFlatten:
1288
1382
  resolved.staticConfig.acceptsClassName !== false &&
1383
+ !resolved.staticConfig.isHOC &&
1289
1384
  !resolved.staticConfig.neverFlatten &&
1290
1385
  !resolved.staticConfig.context,
1291
1386
  staticConfig: resolved.staticConfig,
@@ -1324,7 +1419,6 @@ export function createTamaguiCompilerHost(
1324
1419
  'group',
1325
1420
  'transition',
1326
1421
  'animation',
1327
- 'animateOnly',
1328
1422
  'animatePresence',
1329
1423
  'animatedBy',
1330
1424
  'render',
@@ -1357,6 +1451,21 @@ export function createTamaguiCompilerHost(
1357
1451
  if (dynamic?.type !== 'string' || !dynamic.values?.length) return null
1358
1452
  return dynamic.values.map(String)
1359
1453
  }
1454
+ // an opaque native value the compiler can still lower: a finite domain
1455
+ // (a static array member, a template over one) resolves each value like a
1456
+ // conditional branch and selects it by the runtime value; an unbounded
1457
+ // number passes straight into the style key once probing proves the prop
1458
+ // maps numbers to that one key unchanged
1459
+ const nativeDynamicShape = (
1460
+ name: string,
1461
+ component: LoweringComponent,
1462
+ dynamic: DynamicEvaluation | null | undefined
1463
+ ): 'domain' | 'number' | null => {
1464
+ if (!dynamic || !canLowerConditionalStyleProp(name, component)) return null
1465
+ if (dynamic.values?.length) return 'domain'
1466
+ if (dynamic.type === 'number' && directStyleName(name, component)) return 'number'
1467
+ return null
1468
+ }
1360
1469
  type DynamicStyleEntry = Extract<
1361
1470
  MaterializedElement['entries'][number],
1362
1471
  { kind: 'prop' }
@@ -1373,11 +1482,17 @@ export function createTamaguiCompilerHost(
1373
1482
  canLowerConditionalStyleProp(entry.name, component)) ||
1374
1483
  frontendClassDomain(entry, component) !== null
1375
1484
 
1485
+ const expandStyleShorthand = (name: string): string =>
1486
+ options.tamaguiConfig.shorthands?.[name] ?? name
1487
+
1376
1488
  const isInvalidHostStyleProp = (
1377
1489
  name: string,
1378
1490
  component: LoweringComponent
1379
1491
  ): boolean => {
1492
+ const expanded = expandStyleShorthand(name)
1380
1493
  const staticConfig = component.staticConfig as StaticConfig
1494
+ // explicit variants and inline props retain their component owners.
1495
+ if (staticConfig.inlineProps?.has(name) || staticConfig.variants?.[name]) return false
1381
1496
  const validStyles =
1382
1497
  staticConfig.validStyles ||
1383
1498
  (staticConfig.isInput
@@ -1385,7 +1500,7 @@ export function createTamaguiCompilerHost(
1385
1500
  : staticConfig.isText
1386
1501
  ? stylePropsText
1387
1502
  : validStylesView)
1388
- return name in stylePropsAll && !isValidStyleKey(name, validStyles)
1503
+ return expanded in stylePropsAll && !isValidStyleKey(expanded, validStyles)
1389
1504
  }
1390
1505
 
1391
1506
  const directStyleName = (name: string, component: LoweringComponent): string | null => {
@@ -1394,7 +1509,7 @@ export function createTamaguiCompilerHost(
1394
1509
  }
1395
1510
  const staticConfig = component.staticConfig as StaticConfig
1396
1511
  if (staticConfig.variants?.[name]) return null
1397
- const expanded = options.tamaguiConfig.shorthands?.[name] ?? name
1512
+ const expanded = expandStyleShorthand(name)
1398
1513
  return staticConfig.validStyles?.[expanded] ? expanded : null
1399
1514
  }
1400
1515
 
@@ -1620,7 +1735,7 @@ export function createTamaguiCompilerHost(
1620
1735
  return {
1621
1736
  resolveComponent: resolve,
1622
1737
  isStyleProp,
1623
- canLowerDynamicStyleProp(name, component, valueKind) {
1738
+ canLowerDynamicStyleProp(name, component, valueKind, dynamic) {
1624
1739
  // a conditional with static branches lowers per-branch on both
1625
1740
  // platforms: each side resolves at compile time, only the test survives
1626
1741
  if (
@@ -1646,7 +1761,10 @@ export function createTamaguiCompilerHost(
1646
1761
  return (
1647
1762
  !options.disablePartialExtraction &&
1648
1763
  ((platform === 'web' && !!directStyleName(name, component)) ||
1649
- (platform === 'native' && directStyleName(name, component) === 'opacity'))
1764
+ (platform === 'native' &&
1765
+ (directStyleName(name, component) === 'opacity' ||
1766
+ (valueKind === 'bailout' &&
1767
+ nativeDynamicShape(name, component, dynamic) !== null))))
1650
1768
  )
1651
1769
  },
1652
1770
  developmentDebugInstrumentation,
@@ -1825,8 +1943,12 @@ export function createTamaguiCompilerHost(
1825
1943
  // DOM primitives (html.*) keep their own event contract (see
1826
1944
  // @tamagui/dom events table) — only bail for tamagui components,
1827
1945
  // whose pointer handlers exist solely via usePointerEvents.
1946
+ // onLayout on web exists only through the runtime's
1947
+ // useElementLayout; a flattened div drops it. a flattened native
1948
+ // view reports layout itself.
1828
1949
  const needsRuntimeMapping = (name: string) =>
1829
1950
  runtimeEventProps.has(name) ||
1951
+ (platform === 'web' && name === 'onLayout') ||
1830
1952
  (platform === 'native' &&
1831
1953
  !component.domTag &&
1832
1954
  nativePointerEventProps.has(name))
@@ -1848,7 +1970,7 @@ export function createTamaguiCompilerHost(
1848
1970
  // A styled() definition's animation props decide the same things the call
1849
1971
  // site's do, so the lowering decision reads both. completeProps merges
1850
1972
  // them far below, long after this point, and reading only the call site
1851
- // here decided a definition's transition/animation/animateOnly as if it
1973
+ // here decided a definition's transition/animation props as if they
1852
1974
  // had never been written: the element flattened and the prop was dropped
1853
1975
  // with no diagnostic. Same merge function and same precedence as
1854
1976
  // completeProps, so there is one answer to "what is this prop's value".
@@ -1864,27 +1986,6 @@ export function createTamaguiCompilerHost(
1864
1986
  (name) => runtimeAnimationProps.has(name) && animationProps[name] !== undefined
1865
1987
  ),
1866
1988
  ])
1867
- const animateOnlyEntry = input.element.entries.find(
1868
- (entry) => entry.kind === 'prop' && entry.name === 'animateOnly'
1869
- )
1870
- if (animationNames.has('animateOnly')) {
1871
- return bailout(
1872
- input,
1873
- 'local/unsupported-target',
1874
- 'Animated candidates remain on the runtime path',
1875
- animateOnlyEntry?.span,
1876
- {
1877
- rule: 5,
1878
- message: zeroRuleMessage(5, {
1879
- // without the origin an author reads this against JSX that does
1880
- // not carry the prop, because the styled() definition does
1881
- detail: animateOnlyEntry
1882
- ? `animateOnly on ${input.element.component.name}`
1883
- : `animateOnly in the styled() definition of ${input.element.component.name}`,
1884
- }),
1885
- }
1886
- )
1887
- }
1888
1989
  const transitionEntry = input.element.entries.find(
1889
1990
  (entry) => entry.kind === 'prop' && entry.name === 'transition'
1890
1991
  )
@@ -1936,8 +2037,7 @@ export function createTamaguiCompilerHost(
1936
2037
  const runtimeAnimationRequired =
1937
2038
  (animationNames.has('transition') && resolvedCssTransition === null) ||
1938
2039
  [...animationNames].some(
1939
- (name) =>
1940
- name !== 'transition' && name !== 'animatedBy' && name !== 'animateOnly'
2040
+ (name) => name !== 'transition' && name !== 'animatedBy'
1941
2041
  ) ||
1942
2042
  animatedByNeedsRuntime
1943
2043
  let dynamicHostStyleProperties: string[] | null = null
@@ -2311,7 +2411,9 @@ export function createTamaguiCompilerHost(
2311
2411
  entry.kind === 'prop' &&
2312
2412
  (directStyleName(entry.name, component) === 'opacity' ||
2313
2413
  (entry.value.kind === 'conditional' &&
2314
- canLowerConditionalStyleProp(entry.name, component)))
2414
+ canLowerConditionalStyleProp(entry.name, component)) ||
2415
+ (entry.value.kind === 'bailout' &&
2416
+ nativeDynamicShape(entry.name, component, entry.value.dynamic) !== null))
2315
2417
  )
2316
2418
  if (
2317
2419
  dynamicStyleEntries.length > 0 &&
@@ -2491,6 +2593,52 @@ export function createTamaguiCompilerHost(
2491
2593
  'getSplitStyles returned no static result'
2492
2594
  )
2493
2595
  }
2596
+ const nativeMetrics = platform === 'native' ? nativeTextMetrics(split) : null
2597
+ if (nativeMetrics) nativeTextMetricsByElement.set(input.element, nativeMetrics)
2598
+ const hasDynamicFontSize = dynamicStyleEntries.some((entry) => {
2599
+ const name = directStyleName(entry.name, component)
2600
+ return name === 'fontFamily' || name === 'fontSize'
2601
+ })
2602
+ const hasUnresolvedNativeLineHeight =
2603
+ typeof nativeMetrics?.lineHeight === 'number' &&
2604
+ nativeMetrics.fontSize === undefined &&
2605
+ !hasDynamicFontSize
2606
+ const hasDynamicTextMetric = dynamicStyleEntries.some((entry) => {
2607
+ const name = directStyleName(entry.name, component)
2608
+ return (
2609
+ name === 'fontFamily' ||
2610
+ name === 'fontSize' ||
2611
+ name === 'lineHeight' ||
2612
+ (name === null && !!component.staticConfig.variants?.[entry.name])
2613
+ )
2614
+ })
2615
+ const changesInheritedNativeFontSize =
2616
+ nativeMetrics?.fontSize !== undefined && nativeMetrics.lineHeight === undefined
2617
+ // flattened core Text goes straight to react-native Text, which cannot
2618
+ // carry this semantic record to a runtime child. the component path
2619
+ // completes it once through the shared metric context.
2620
+ if (
2621
+ platform === 'native' &&
2622
+ component.staticConfig.isText &&
2623
+ !component.domTag &&
2624
+ (hasUnresolvedNativeLineHeight ||
2625
+ changesInheritedNativeFontSize ||
2626
+ (hasChildThatMayRenderReactNode(input.element) &&
2627
+ (nativeMetrics !== null || hasDynamicTextMetric)) ||
2628
+ input.module.elements.some(
2629
+ (ancestor) =>
2630
+ ancestor !== input.element &&
2631
+ ancestor.span.start < input.element.span.start &&
2632
+ ancestor.span.end > input.element.span.end &&
2633
+ nativeTextMetricsByElement.has(ancestor)
2634
+ ))
2635
+ ) {
2636
+ return bailout(
2637
+ input,
2638
+ 'local/unsupported-target',
2639
+ 'Native text metrics need the runtime Text provider for this subtree'
2640
+ )
2641
+ }
2494
2642
  if (
2495
2643
  split.programLifecycleStyleKeys?.enter?.size ||
2496
2644
  split.programLifecycleStyleKeys?.exit?.size
@@ -2677,9 +2825,15 @@ export function createTamaguiCompilerHost(
2677
2825
  input.source,
2678
2826
  `__TamaguiNativeStyle${input.element.span.start}`
2679
2827
  )
2828
+ // the native style engine pushes the runtime's exact keys, so its base keeps them
2829
+ const usesNativeFastPath =
2830
+ !!themedStyleKeys &&
2831
+ !!options.experimentalNativeFastPath &&
2832
+ dynamicStyleEntries.length === 0 &&
2833
+ !input.element.entries.some((entry) => entry.kind === 'spread')
2680
2834
  const nativeStyleImports = [
2681
2835
  {
2682
- content: `\nfunction ${nativeStyleLocal}() { return ${nativeStyleLocal}._ ?? (${nativeStyleLocal}._ = ${JSON.stringify(nativeStyle ?? {})}); } ${nativeStyleLocal}();`,
2836
+ content: `\nfunction ${nativeStyleLocal}() { return ${nativeStyleLocal}._ ?? (${nativeStyleLocal}._ = ${JSON.stringify((usesNativeFastPath ? nativeStyle : collapseNativeShorthands(nativeStyle)) ?? {})}); } ${nativeStyleLocal}();`,
2683
2837
  origin: input.element.component.span,
2684
2838
  },
2685
2839
  ]
@@ -2690,12 +2844,7 @@ export function createTamaguiCompilerHost(
2690
2844
  mappingLocal: string
2691
2845
  }
2692
2846
  | undefined
2693
- if (
2694
- themedStyleKeys &&
2695
- options.experimentalNativeFastPath &&
2696
- dynamicStyleEntries.length === 0 &&
2697
- !input.element.entries.some((entry) => entry.kind === 'spread')
2698
- ) {
2847
+ if (usesNativeFastPath) {
2699
2848
  const mappingLocal = unusedIdentifier(
2700
2849
  input.source,
2701
2850
  `__TamaguiNativeMapping${input.element.span.start}`
@@ -2732,6 +2881,7 @@ export function createTamaguiCompilerHost(
2732
2881
  }
2733
2882
  styleEntries = [...new Set([...styleEntries, ...propsResult.consumed])]
2734
2883
  let nativeDOMStyleSource = nativeStyleSource
2884
+ let nativeDOMMetrics = nativeMetrics
2735
2885
  let runtimeStyleSource: string | null = null
2736
2886
  if (
2737
2887
  domStyleProgram?.kind === 'prop' &&
@@ -2795,6 +2945,10 @@ export function createTamaguiCompilerHost(
2795
2945
  item.value.span
2796
2946
  )
2797
2947
  }
2948
+ nativeDOMMetrics = mergeNativeTextMetrics(
2949
+ nativeDOMMetrics,
2950
+ nativeTextMetrics(itemSplit)
2951
+ )
2798
2952
  value = JSON.stringify(itemStyle ?? {})
2799
2953
  }
2800
2954
  const condition = item.condition
@@ -2819,6 +2973,11 @@ export function createTamaguiCompilerHost(
2819
2973
  ? `__styles={${runtimeStyleSource}}`
2820
2974
  : `__styles: ${runtimeStyleSource}`
2821
2975
  : '',
2976
+ nativeDOMMetrics
2977
+ ? serializedProps(input.element.form, [
2978
+ ['__textMetrics', JSON.stringify(nativeDOMMetrics)],
2979
+ ])
2980
+ : '',
2822
2981
  serializedProps(input.element.form, propsResult.additions),
2823
2982
  ]
2824
2983
  .filter(Boolean)
@@ -3061,7 +3220,77 @@ export function createTamaguiCompilerHost(
3061
3220
  const baseStyleForDiff = staticObject(nativeStyleResolved)
3062
3221
  ? nativeStyleResolved
3063
3222
  : {}
3223
+ // resolves the element with one value in place of the dynamic prop
3224
+ // and returns what that value adds over the static style
3225
+ const valueDiff = (
3226
+ entry: DynamicStyleEntry,
3227
+ value: unknown,
3228
+ what: string
3229
+ ): Record<string, unknown> | string => {
3230
+ const { branchCompleteProps } = propsForConditional(entry, value)
3231
+ const branchSplit = resolveSplitStyles(
3232
+ branchCompleteProps,
3233
+ component.staticConfig,
3234
+ cssAnimationDriver,
3235
+ component.displayName
3236
+ )
3237
+ const branchStyle = branchSplit?.viewProps?.style ?? {}
3238
+ if (!staticObject(branchStyle)) {
3239
+ return `${what} did not resolve to a static native style`
3240
+ }
3241
+ // a value (e.g. through a variant) may change viewProps beyond
3242
+ // style; the style array cannot express those, so any non-style
3243
+ // difference sends the element to the runtime path
3244
+ for (const viewPropsKey of new Set([
3245
+ ...Object.keys(branchSplit?.viewProps ?? {}),
3246
+ ...Object.keys(split.viewProps ?? {}),
3247
+ ])) {
3248
+ if (viewPropsKey === 'style') continue
3249
+ if (
3250
+ JSON.stringify(branchSplit?.viewProps?.[viewPropsKey]) !==
3251
+ JSON.stringify((split.viewProps as any)?.[viewPropsKey])
3252
+ ) {
3253
+ return `${what} changes ${viewPropsKey}, which the compiled style array cannot express`
3254
+ }
3255
+ }
3256
+ for (const key of Object.keys(baseStyleForDiff)) {
3257
+ if (!(key in branchStyle)) {
3258
+ return `${what} removes ${key}, which an additive style array cannot express`
3259
+ }
3260
+ }
3261
+ const diff: Record<string, unknown> = {}
3262
+ for (const [key, value] of Object.entries(branchStyle)) {
3263
+ if (
3264
+ JSON.stringify((baseStyleForDiff as any)[key]) !== JSON.stringify(value)
3265
+ ) {
3266
+ diff[key] = value
3267
+ }
3268
+ }
3269
+ if (!isSerializableNativeStyle(diff) || core.containsThemeRef(diff)) {
3270
+ return `${what} style is not statically representable`
3271
+ }
3272
+ return diff
3273
+ }
3274
+ // each dynamic prop owns the keys it contributes; two props writing
3275
+ // one key would need their source order resolved at runtime
3276
+ const claimKeys = (entry: DynamicStyleEntry, keys: Iterable<string>) => {
3277
+ for (const key of keys) {
3278
+ if (conditionalKeys.has(key)) {
3279
+ return bailout(
3280
+ input,
3281
+ 'local/dynamic-style-value',
3282
+ `Multiple dynamic props contribute ${key}; their interaction cannot be resolved at compile time`,
3283
+ entry.value.span
3284
+ )
3285
+ }
3286
+ }
3287
+ return null
3288
+ }
3064
3289
  for (const entry of dynamicStyleEntries) {
3290
+ const shape =
3291
+ entry.value.kind === 'bailout'
3292
+ ? nativeDynamicShape(entry.name, component, entry.value.dynamic)
3293
+ : null
3065
3294
  // opacity keeps the leaner inline-expression form even for
3066
3295
  // ternaries; per-branch lowering is for props that form cannot carry
3067
3296
  if (
@@ -3075,79 +3304,21 @@ export function createTamaguiCompilerHost(
3075
3304
  Record<string, unknown>
3076
3305
  >()
3077
3306
  for (const leaf of leaves) {
3078
- const { branchCompleteProps } = propsForConditional(entry, leaf.value)
3079
- const branchSplit = resolveSplitStyles(
3080
- branchCompleteProps,
3081
- component.staticConfig,
3082
- cssAnimationDriver,
3083
- component.displayName
3307
+ const diff = valueDiff(
3308
+ entry,
3309
+ leaf.value,
3310
+ `Conditional ${entry.name} branch`
3084
3311
  )
3085
- const branchStyle = branchSplit?.viewProps?.style ?? {}
3086
- if (!staticObject(branchStyle)) {
3087
- return bailout(
3088
- input,
3089
- 'local/dynamic-style-value',
3090
- `Conditional ${entry.name} branch did not resolve to a static native style`,
3091
- entry.value.span
3092
- )
3093
- }
3094
- // a branch (e.g. through a variant) may change viewProps
3095
- // beyond style; the style array cannot express those, so any
3096
- // non-style difference sends the element to the runtime path
3097
- for (const viewPropsKey of new Set([
3098
- ...Object.keys(branchSplit?.viewProps ?? {}),
3099
- ...Object.keys(split.viewProps ?? {}),
3100
- ])) {
3101
- if (viewPropsKey === 'style') continue
3102
- if (
3103
- JSON.stringify(branchSplit?.viewProps?.[viewPropsKey]) !==
3104
- JSON.stringify((split.viewProps as any)?.[viewPropsKey])
3105
- ) {
3106
- return bailout(
3107
- input,
3108
- 'local/dynamic-style-value',
3109
- `Conditional ${entry.name} branch changes ${viewPropsKey}, which the compiled style array cannot express`,
3110
- entry.value.span
3111
- )
3112
- }
3113
- }
3114
- for (const key of Object.keys(baseStyleForDiff)) {
3115
- if (!(key in branchStyle)) {
3116
- return bailout(
3117
- input,
3118
- 'local/dynamic-style-value',
3119
- `Conditional ${entry.name} branch removes ${key}, which an additive style array cannot express`,
3120
- entry.value.span
3121
- )
3122
- }
3123
- }
3124
- const diff: Record<string, unknown> = {}
3125
- for (const [key, value] of Object.entries(branchStyle)) {
3126
- if (
3127
- JSON.stringify((baseStyleForDiff as any)[key]) !==
3128
- JSON.stringify(value)
3129
- ) {
3130
- diff[key] = value
3131
- }
3132
- }
3133
- if (!isSerializableNativeStyle(diff) || core.containsThemeRef(diff)) {
3312
+ if (typeof diff === 'string') {
3134
3313
  return bailout(
3135
3314
  input,
3136
3315
  'local/dynamic-style-value',
3137
- `Conditional ${entry.name} branch style is not statically representable`,
3316
+ diff,
3138
3317
  entry.value.span
3139
3318
  )
3140
3319
  }
3141
- for (const key of Object.keys(diff)) {
3142
- if (conditionalKeys.has(key)) {
3143
- return bailout(
3144
- input,
3145
- 'local/dynamic-style-value',
3146
- `Multiple conditionals contribute ${key}; their interaction cannot be resolved per-branch`,
3147
- entry.value.span
3148
- )
3149
- }
3150
- }
3320
+ const conflict = claimKeys(entry, Object.keys(diff))
3321
+ if (conflict) return conflict
3151
3322
  leafDiffs.set(leaf, diff)
3152
3323
  }
3153
3324
  for (const diff of leafDiffs.values()) {
@@ -3165,14 +3336,70 @@ export function createTamaguiCompilerHost(
3165
3336
  }
3166
3337
 
3167
3338
  conditionalParts.push(serializeNativeTree(tree))
3168
- } else {
3339
+ } else if (shape === 'domain' && entry.value.kind === 'bailout') {
3340
+ // a finite domain is a lookup from each runtime value to its diff
3341
+ const lookup: string[] = []
3342
+ const keys = new Set<string>()
3343
+ for (const value of entry.value.dynamic!.values!) {
3344
+ const diff = valueDiff(
3345
+ entry,
3346
+ value,
3347
+ `${entry.name} value ${JSON.stringify(value)}`
3348
+ )
3349
+ if (typeof diff === 'string') {
3350
+ return bailout(
3351
+ input,
3352
+ 'local/dynamic-style-value',
3353
+ diff,
3354
+ entry.value.span
3355
+ )
3356
+ }
3357
+ for (const key of Object.keys(diff)) keys.add(key)
3358
+ lookup.push(`${JSON.stringify(String(value))}: ${JSON.stringify(diff)}`)
3359
+ }
3360
+ const conflict = claimKeys(entry, keys)
3361
+ if (conflict) return conflict
3362
+ for (const key of keys) conditionalKeys.add(key)
3169
3363
  const index = expressions.length
3170
3364
  expressions.push(
3171
3365
  input.source.slice(entry.value.span.start, entry.value.span.end)
3172
3366
  )
3173
- plainDynamicParts.push(
3174
- `${JSON.stringify(directStyleName(entry.name, component))}: expressions[${index}]`
3367
+ conditionalParts.push(`({ ${lookup.join(', ')} })[expressions[${index}]]`)
3368
+ } else {
3369
+ const key = directStyleName(entry.name, component)!
3370
+ if (shape === 'number') {
3371
+ // two probes prove a number lands unchanged in exactly this key
3372
+ for (const probe of [7.25, 1031]) {
3373
+ const diff = valueDiff(entry, probe, `${entry.name} number`)
3374
+ if (
3375
+ typeof diff === 'string' ||
3376
+ JSON.stringify(diff) !== JSON.stringify({ [key]: probe })
3377
+ ) {
3378
+ return bailout(
3379
+ input,
3380
+ 'local/dynamic-style-value',
3381
+ `Style prop ${entry.name} transforms numbers on native, so its runtime value cannot pass through`,
3382
+ entry.value.span
3383
+ )
3384
+ }
3385
+ }
3386
+ if (key in baseStyleForDiff) {
3387
+ return bailout(
3388
+ input,
3389
+ 'local/dynamic-style-value',
3390
+ `Style prop ${entry.name} overlaps a static ${key}`,
3391
+ entry.value.span
3392
+ )
3393
+ }
3394
+ const conflict = claimKeys(entry, [key])
3395
+ if (conflict) return conflict
3396
+ conditionalKeys.add(key)
3397
+ }
3398
+ const index = expressions.length
3399
+ expressions.push(
3400
+ input.source.slice(entry.value.span.start, entry.value.span.end)
3175
3401
  )
3402
+ plainDynamicParts.push(`${JSON.stringify(key)}: expressions[${index}]`)
3176
3403
  }
3177
3404
  }
3178
3405
  const dynamicStyle = plainDynamicParts.join(', ')
@@ -3362,7 +3589,7 @@ export function createTamaguiCompilerHost(
3362
3589
  split,
3363
3590
  props,
3364
3591
  options.tamaguiConfig,
3365
- !component.domTag,
3592
+ true,
3366
3593
  Boolean(component.staticConfig.styleFrontend)
3367
3594
  )
3368
3595
  const className = artifacts.className
@@ -3564,7 +3791,7 @@ export function createTamaguiCompilerHost(
3564
3791
  branchSplit,
3565
3792
  branchProps,
3566
3793
  options.tamaguiConfig,
3567
- !component.domTag,
3794
+ true,
3568
3795
  Boolean(component.staticConfig.styleFrontend)
3569
3796
  )
3570
3797
  leafArtifactsMap.set(leaf, {