@tamagui/static 3.0.0-beta.831.1 → 3.0.0-beta.889.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 (41) hide show
  1. package/dist/cjs/compiler.cjs +17 -5
  2. package/dist/cjs/compilerHost.cjs +148 -39
  3. package/dist/cjs/componentDiscovery.cjs +94 -0
  4. package/dist/cjs/domStructuralPass.cjs +12 -2
  5. package/dist/cjs/exports.cjs +4 -1
  6. package/dist/cjs/extractor/bundleConfig.cjs +23 -0
  7. package/dist/cjs/zero/theme.cjs +5 -20
  8. package/dist/esm/compiler.mjs +18 -6
  9. package/dist/esm/compiler.mjs.map +1 -1
  10. package/dist/esm/compilerHost.mjs +146 -40
  11. package/dist/esm/compilerHost.mjs.map +1 -1
  12. package/dist/esm/componentDiscovery.mjs +69 -0
  13. package/dist/esm/componentDiscovery.mjs.map +1 -0
  14. package/dist/esm/domStructuralPass.mjs +12 -2
  15. package/dist/esm/domStructuralPass.mjs.map +1 -1
  16. package/dist/esm/exports.mjs +4 -1
  17. package/dist/esm/extractor/bundleConfig.mjs +23 -1
  18. package/dist/esm/extractor/bundleConfig.mjs.map +1 -1
  19. package/dist/esm/zero/theme.mjs +5 -20
  20. package/dist/esm/zero/theme.mjs.map +1 -1
  21. package/package.json +17 -17
  22. package/src/compiler.ts +27 -5
  23. package/src/compilerHost.ts +262 -74
  24. package/src/componentDiscovery.ts +105 -0
  25. package/src/domStructuralPass.ts +22 -2
  26. package/src/exports.ts +2 -0
  27. package/src/extractor/bundleConfig.ts +34 -0
  28. package/src/zero/theme.ts +12 -21
  29. package/types/compiler.d.ts +13 -0
  30. package/types/compiler.d.ts.map +1 -1
  31. package/types/compilerHost.d.ts +13 -0
  32. package/types/compilerHost.d.ts.map +1 -1
  33. package/types/componentDiscovery.d.ts +27 -0
  34. package/types/componentDiscovery.d.ts.map +1 -0
  35. package/types/domStructuralPass.d.ts.map +1 -1
  36. package/types/exports.d.ts +2 -0
  37. package/types/exports.d.ts.map +1 -1
  38. package/types/extractor/bundleConfig.d.ts +7 -0
  39. package/types/extractor/bundleConfig.d.ts.map +1 -1
  40. package/types/zero/theme.d.ts +0 -1
  41. package/types/zero/theme.d.ts.map +1 -1
@@ -21,6 +21,7 @@ import {
21
21
  StyleObjectRules,
22
22
  StyleObjectValue,
23
23
  stylePropsAll,
24
+ stylePropsInput,
24
25
  stylePropsText,
25
26
  validStyles as validStylesView,
26
27
  } from '@tamagui/helpers'
@@ -55,11 +56,38 @@ export interface CompilerComponentModule {
55
56
  resolvedId: string
56
57
  }
57
58
 
59
+ /**
60
+ * The components lowering can resolve, keyed the way element provenance names
61
+ * them. The frontend owns one per compile and grows it as it discovers component
62
+ * modules outside the configured `components` list.
63
+ */
64
+ export interface CompilerComponentRegistry {
65
+ /** host-resolved module id -> the module name its components register under */
66
+ modulesById: Map<string, string>
67
+ componentsByModule: Map<string, LoadedComponents>
68
+ }
69
+
70
+ export function createComponentRegistry(
71
+ components: readonly LoadedComponents[],
72
+ componentModules: readonly CompilerComponentModule[]
73
+ ): CompilerComponentRegistry {
74
+ return {
75
+ modulesById: new Map(
76
+ componentModules.map((module) => [module.resolvedId, module.moduleName])
77
+ ),
78
+ componentsByModule: new Map(
79
+ components.map((component) => [component.moduleName, component])
80
+ ),
81
+ }
82
+ }
83
+
58
84
  export interface TamaguiCompilerHostOptions {
59
85
  target: CompilerTarget
60
86
  tamaguiConfig: TamaguiInternalConfig
61
87
  components: LoadedComponents[]
62
88
  componentModules: CompilerComponentModule[]
89
+ /** shared with the frontend so discovery during `prepare` is visible to this host */
90
+ registry?: CompilerComponentRegistry
63
91
  /** Keep elements with dynamic style props fully on the runtime path. */
64
92
  disablePartialExtraction?: boolean
65
93
  /** emit native theme-token mappings for the native style engine */
@@ -390,6 +418,11 @@ const runtimeAnimationProps = new Set([
390
418
  'animatedBy',
391
419
  ])
392
420
 
421
+ // createComponent reads these and never forwards them to the host element, so a
422
+ // flattened element has to drop them instead of emitting an unknown attribute.
423
+ // only the inert values reach the emit: an active one already bailed.
424
+ const componentOnlyProps = new Set(['asChild', 'disableOptimization', 'themeInverse'])
425
+
393
426
  const cssShorthandConflicts: Record<string, readonly string[]> = {
394
427
  background: [
395
428
  'backgroundAttachment',
@@ -548,6 +581,12 @@ const cssShorthandConflicts: Record<string, readonly string[]> = {
548
581
  }
549
582
 
550
583
  const cssConflictFamilies = [
584
+ new Set(['width', 'inlineSize']),
585
+ new Set(['minWidth', 'minInlineSize']),
586
+ new Set(['maxWidth', 'maxInlineSize']),
587
+ new Set(['height', 'blockSize']),
588
+ new Set(['minHeight', 'minBlockSize']),
589
+ new Set(['maxHeight', 'maxBlockSize']),
551
590
  new Set([
552
591
  'marginInline',
553
592
  'marginInlineEnd',
@@ -943,6 +982,8 @@ export function createTamaguiCompilerHost(
943
982
  ): CompilerLoweringHost {
944
983
  const platform = options.target === 'native' ? 'native' : 'web'
945
984
  const core = requireTamaguiCore(platform) as any
985
+ const compilerVariantStyleResolver = core.styled(core.View, {}).staticConfig
986
+ .variantStyleResolver
946
987
  const firstThemeName = Object.keys(options.tamaguiConfig.themes ?? {})[0] ?? ''
947
988
  const firstTheme = options.tamaguiConfig.themes?.[firstThemeName] ?? {}
948
989
  const theme = firstTheme
@@ -968,6 +1009,39 @@ export function createTamaguiCompilerHost(
968
1009
  }
969
1010
  return false
970
1011
  }
1012
+ // A native bundle knows one thing about its platform: it is not web. So a
1013
+ // `web:` clause can never match there and a bare `native:` clause always
1014
+ // does, which makes a value built only from those two resolvable at compile
1015
+ // time. Every other modifier is live at render: media and container queries
1016
+ // measure, theme and group clauses read context, state and lifecycle clauses
1017
+ // track the component, and the device platforms (ios, android, tv) are not
1018
+ // known until the bundle runs. One of those anywhere in the value keeps the
1019
+ // whole element on the runtime path.
1020
+ const platformModifiersRegistered =
1021
+ modifierRegistry.get('web') === 'platform' &&
1022
+ modifierRegistry.get('native') === 'platform'
1023
+ const nativeClauseValue = (
1024
+ value: unknown
1025
+ ): 'live' | { payload: unknown; matched: boolean } | null => {
1026
+ if (typeof value !== 'string' || !flatClausePattern.test(value)) {
1027
+ return isClauseObjectValue(value) ? 'live' : null
1028
+ }
1029
+ if (!platformModifiersRegistered) return 'live'
1030
+ const parsed = parseValue(value, modifierRegistry)
1031
+ if (!parsed.ok || parsed.value.clauses.length === 0) return 'live'
1032
+ let payload: unknown = parsed.value.base
1033
+ let matched = parsed.value.base !== null
1034
+ for (const clause of parsed.value.clauses) {
1035
+ // a chain containing `web` is dead here whatever else it names
1036
+ if (clause.modifiers.includes('web')) continue
1037
+ if (clause.modifiers.length !== 1 || clause.modifiers[0] !== 'native') return 'live'
1038
+ // platform clauses all rank the same, so a later one wins and any of them
1039
+ // beats the unconditional base
1040
+ payload = clause.payload
1041
+ matched = true
1042
+ }
1043
+ return { payload, matched }
1044
+ }
971
1045
  const configuredAnimationDriver = options.tamaguiConfig.animations as
972
1046
  | AnimationDriver
973
1047
  | undefined
@@ -1031,12 +1105,9 @@ export function createTamaguiCompilerHost(
1031
1105
  }
1032
1106
  return resolved.join(' ')
1033
1107
  }
1034
- const modulesById = new Map(
1035
- options.componentModules.map((module) => [module.resolvedId, module.moduleName])
1036
- )
1037
- const componentsByModule = new Map(
1038
- options.components.map((component) => [component.moduleName, component])
1039
- )
1108
+ const { modulesById, componentsByModule } =
1109
+ options.registry ??
1110
+ createComponentRegistry(options.components, options.componentModules)
1040
1111
 
1041
1112
  // A component's import provenance chose its authoring syntax, and the frontend
1042
1113
  // descriptor frozen onto its static config is the only thing that knows how to
@@ -1122,7 +1193,10 @@ export function createTamaguiCompilerHost(
1122
1193
  isInput: row.backing === 'textinput',
1123
1194
  // CSS text properties may be authored on any element and inherited by
1124
1195
  // descendant text, including from a View-backed tag on native.
1125
- validStyles: { ...validStylesView, ...stylePropsText },
1196
+ validStyles: {
1197
+ ...validStylesView,
1198
+ ...(row.backing === 'textinput' ? stylePropsInput : stylePropsText),
1199
+ },
1126
1200
  defaultProps: {
1127
1201
  ...base.defaultProps,
1128
1202
  ...platformDefaults,
@@ -1135,6 +1209,13 @@ export function createTamaguiCompilerHost(
1135
1209
  definition: MaterializedStyledDefinition | null
1136
1210
  ): { key: string; staticConfig: StaticConfig; displayName?: string } | null => {
1137
1211
  if (!definition || definition.options.kind !== 'static') return null
1212
+ const staticConfig = definition.staticConfig
1213
+ if (
1214
+ staticConfig &&
1215
+ (staticConfig.kind !== 'static' || !staticObject(staticConfig.value))
1216
+ ) {
1217
+ return null
1218
+ }
1138
1219
  const base = directStaticConfig({
1139
1220
  kind: 'element',
1140
1221
  form: 'jsx',
@@ -1160,29 +1241,32 @@ export function createTamaguiCompilerHost(
1160
1241
  typeof definition.baseClassName.value === 'string'
1161
1242
  ? definition.baseClassName.value
1162
1243
  : undefined
1244
+ const localStaticConfig = {
1245
+ ...base.staticConfig,
1246
+ ...(staticConfig?.value as Record<string, unknown> | undefined),
1247
+ variants: {
1248
+ ...base.staticConfig.variants,
1249
+ ...(variants as object | undefined),
1250
+ },
1251
+ defaultProps: {
1252
+ ...base.staticConfig.defaultProps,
1253
+ ...defaultProps,
1254
+ ...(defaultVariants as object | undefined),
1255
+ },
1256
+ defaultVariants,
1257
+ baseClassName: [base.staticConfig.baseClassName, baseClassName]
1258
+ .filter(Boolean)
1259
+ .join(' '),
1260
+ context: context ?? base.staticConfig.context,
1261
+ contextProps: context
1262
+ ? contextProps
1263
+ : (contextProps ?? base.staticConfig.contextProps),
1264
+ }
1265
+ ;(localStaticConfig as any).variantStyleResolver = compilerVariantStyleResolver
1163
1266
  return {
1164
1267
  key: componentKey(definition.id, definition.name),
1165
1268
  displayName: displayName || base.displayName,
1166
- staticConfig: normalizeStaticConfig({
1167
- ...base.staticConfig,
1168
- variants: {
1169
- ...base.staticConfig.variants,
1170
- ...(variants as object | undefined),
1171
- },
1172
- defaultProps: {
1173
- ...base.staticConfig.defaultProps,
1174
- ...defaultProps,
1175
- ...(defaultVariants as object | undefined),
1176
- },
1177
- defaultVariants,
1178
- baseClassName: [base.staticConfig.baseClassName, baseClassName]
1179
- .filter(Boolean)
1180
- .join(' '),
1181
- context: context ?? base.staticConfig.context,
1182
- contextProps: context
1183
- ? contextProps
1184
- : (contextProps ?? base.staticConfig.contextProps),
1185
- }),
1269
+ staticConfig: normalizeStaticConfig(localStaticConfig),
1186
1270
  }
1187
1271
  }
1188
1272
 
@@ -1191,8 +1275,11 @@ export function createTamaguiCompilerHost(
1191
1275
  styledDefinition: MaterializedStyledDefinition | null
1192
1276
  ): TamaguiLoweringComponent | null => {
1193
1277
  const dom = domStaticConfig(element)
1278
+ // a config the host evaluated is the component's real static config. The
1279
+ // one derived from a visible `styled()` literal only stands in for
1280
+ // definitions nothing evaluated, such as the app's own
1194
1281
  const resolved =
1195
- dom ?? styledStaticConfig(styledDefinition) ?? directStaticConfig(element)
1282
+ dom ?? directStaticConfig(element) ?? styledStaticConfig(styledDefinition)
1196
1283
  if (!resolved) return null
1197
1284
  const defaultProps = resolved.staticConfig.defaultProps ?? {}
1198
1285
  return {
@@ -1254,10 +1341,12 @@ export function createTamaguiCompilerHost(
1254
1341
  const staticConfig = component.staticConfig as StaticConfig
1255
1342
  const validStyles =
1256
1343
  staticConfig.validStyles ||
1257
- (staticConfig.isText || staticConfig.isInput ? stylePropsText : validStylesView)
1258
- return (
1259
- name in stylePropsAll && !isValidStyleKey(name, validStyles, staticConfig.accept)
1260
- )
1344
+ (staticConfig.isInput
1345
+ ? stylePropsInput
1346
+ : staticConfig.isText
1347
+ ? stylePropsText
1348
+ : validStylesView)
1349
+ return name in stylePropsAll && !isValidStyleKey(name, validStyles)
1261
1350
  }
1262
1351
 
1263
1352
  const directStyleName = (name: string, component: LoweringComponent): string | null => {
@@ -1292,6 +1381,7 @@ export function createTamaguiCompilerHost(
1292
1381
  firstThemeName,
1293
1382
  componentState,
1294
1383
  {
1384
+ isStatic: true,
1295
1385
  resolveValues: platform === 'native' ? 'except-theme' : 'variable',
1296
1386
  noClass: platform === 'native',
1297
1387
  isAnimated: false,
@@ -1501,6 +1591,12 @@ export function createTamaguiCompilerHost(
1501
1591
  ) {
1502
1592
  return true
1503
1593
  }
1594
+ // A component resolver receives the complete props object. Without a
1595
+ // compiler-derived read set, resolving an opaque dynamic callsite would
1596
+ // execute the real resolver with an incomplete props snapshot.
1597
+ if ((component.staticConfig as StaticConfig).resolvers?.length) {
1598
+ return false
1599
+ }
1504
1600
  return (
1505
1601
  !options.disablePartialExtraction &&
1506
1602
  ((platform === 'web' && !!directStyleName(name, component)) ||
@@ -1556,15 +1652,56 @@ export function createTamaguiCompilerHost(
1556
1652
  props[entry.name] = entry.value.value
1557
1653
  }
1558
1654
  }
1559
- const disableOptimizationEntry = input.element.entries.find(
1560
- (entry) => entry.kind === 'prop' && entry.name === 'disableOptimization'
1561
- )
1562
- if (disableOptimizationEntry || 'disableOptimization' in props) {
1655
+ if (component.staticConfig.resolvers?.length) {
1656
+ const unresolvedProps = input.element.entries.filter(
1657
+ (entry) => entry.kind === 'prop' && entry.value.kind !== 'static'
1658
+ )
1659
+ // One conditional with static leaves is still a complete prop set:
1660
+ // lowerCandidate evaluates the full resolver chain once per branch.
1661
+ // Multiple unknown props would require a cross-product/read set, so
1662
+ // they remain on the runtime path.
1663
+ const onlyStaticBranchConditional =
1664
+ unresolvedProps.length === 1 &&
1665
+ unresolvedProps[0]?.kind === 'prop' &&
1666
+ unresolvedProps[0].value.kind === 'conditional' &&
1667
+ canLowerConditionalStyleProp(unresolvedProps[0].name, component)
1668
+ const unresolvedProp = onlyStaticBranchConditional
1669
+ ? undefined
1670
+ : unresolvedProps[0]
1671
+ if (unresolvedProp?.kind === 'prop') {
1672
+ return bailout(
1673
+ input,
1674
+ 'local/dynamic-style-value',
1675
+ `Component resolver prop ${unresolvedProp.name} could not be evaluated`,
1676
+ unresolvedProp.span,
1677
+ undefined,
1678
+ unresolvedProp.name
1679
+ )
1680
+ }
1681
+ }
1682
+ // asChild, disableOptimization and themeInverse each asked only whether the
1683
+ // prop was PRESENT, so a false value retained a component that requested
1684
+ // none of the runtime behavior the branch exists for. read the materialized
1685
+ // value instead, and treat a prop the compiler could not evaluate as active.
1686
+ // a later duplicate wins, so the last entry decides and any unevaluated
1687
+ // entry for that name makes the answer unknown.
1688
+ const componentOnlyProp = (name: string) => {
1689
+ let span: MaterializedElement['entries'][number]['span'] | undefined
1690
+ let unknown = false
1691
+ for (const entry of input.element.entries) {
1692
+ if (entry.kind !== 'prop' || entry.name !== name) continue
1693
+ span = entry.span
1694
+ if (entry.value.kind !== 'static') unknown = true
1695
+ }
1696
+ return { unknown, span }
1697
+ }
1698
+ const disableOptimization = componentOnlyProp('disableOptimization')
1699
+ if (disableOptimization.unknown || props.disableOptimization) {
1563
1700
  return bailout(
1564
1701
  input,
1565
1702
  'local/unsupported-target',
1566
1703
  'disableOptimization keeps the component on the runtime path',
1567
- disableOptimizationEntry?.span
1704
+ disableOptimization.span
1568
1705
  )
1569
1706
  }
1570
1707
  if (component.domTag && props.hidden) props.display = 'none'
@@ -1761,11 +1898,7 @@ export function createTamaguiCompilerHost(
1761
1898
  name === 'opacity'
1762
1899
  ? `opacity: (${expression})`
1763
1900
  : `transform: "scale(" + (${expression}) + ")"`
1764
- } else if (
1765
- entry.value.kind === 'bailout' &&
1766
- owners.size === 1 &&
1767
- owners.has(name)
1768
- ) {
1901
+ } else if (entry.value.kind === 'bailout' && owners.has(name)) {
1769
1902
  const dynamic = entry.value.dynamic
1770
1903
  if (dynamic?.type === 'number') {
1771
1904
  property = `${JSON.stringify(name)}: (${expression})`
@@ -1831,8 +1964,13 @@ export function createTamaguiCompilerHost(
1831
1964
  entry.value.kind === 'conditional' &&
1832
1965
  canLowerConditionalStyleProp(entry.name, component)
1833
1966
  )
1834
- if ('theme' in props || 'themeInverse' in props) {
1835
- const themeProp = 'theme' in props ? 'theme' : 'themeInverse'
1967
+ // a theme name of any value selects a theme, while themeInverse only opens a
1968
+ // boundary when it is on
1969
+ const themeEntry = componentOnlyProp('theme')
1970
+ const themeInverseEntry = componentOnlyProp('themeInverse')
1971
+ const themeBoundary = themeEntry.unknown || 'theme' in props
1972
+ if (themeBoundary || themeInverseEntry.unknown || props.themeInverse) {
1973
+ const themeProp = themeBoundary ? 'theme' : 'themeInverse'
1836
1974
  return bailout(
1837
1975
  input,
1838
1976
  'local/unsupported-target',
@@ -1848,15 +1986,13 @@ export function createTamaguiCompilerHost(
1848
1986
  // the single child and emits no element of its own. a flattened host view
1849
1987
  // would add a wrapper the runtime never renders, and the child would
1850
1988
  // never receive the merged props. both platforms.
1851
- const asChildEntry = input.element.entries.find(
1852
- (entry) => entry.kind === 'prop' && entry.name === 'asChild'
1853
- )
1854
- if (asChildEntry || 'asChild' in props) {
1989
+ const asChild = componentOnlyProp('asChild')
1990
+ if (asChild.unknown || props.asChild) {
1855
1991
  return bailout(
1856
1992
  input,
1857
1993
  'local/unsupported-target',
1858
1994
  'asChild renders a Slot, not a host view',
1859
- asChildEntry?.span
1995
+ asChild.span
1860
1996
  )
1861
1997
  }
1862
1998
  // group and container props publish separate keys in the same context:
@@ -2047,7 +2183,11 @@ export function createTamaguiCompilerHost(
2047
2183
  entry.span
2048
2184
  )
2049
2185
  }
2050
- const staticDefaultProps = component.staticConfig.defaultProps ?? {}
2186
+ const resolvedStyleStaticConfig = core.getStyleStaticConfig(
2187
+ component.staticConfig,
2188
+ core.getConfig()
2189
+ )
2190
+ const staticDefaultProps = resolvedStyleStaticConfig.defaultProps ?? {}
2051
2191
  const defaultProps =
2052
2192
  platform === 'web' &&
2053
2193
  !component.staticConfig.isText &&
@@ -2062,6 +2202,19 @@ export function createTamaguiCompilerHost(
2062
2202
  props
2063
2203
  )
2064
2204
  }
2205
+ const domStyleProgram = input.element.entries.find(
2206
+ (entry) =>
2207
+ entry.kind === 'prop' &&
2208
+ entry.name === 'style' &&
2209
+ entry.value.kind === 'dom-style'
2210
+ )
2211
+ if (platform === 'native' && !component.domTag && domStyleProgram) {
2212
+ return bailout(
2213
+ input,
2214
+ 'local/unsupported-target',
2215
+ 'Native style() pieces remain on the runtime path'
2216
+ )
2217
+ }
2065
2218
  const propsForConditional = (
2066
2219
  target: Extract<MaterializedElement['entries'][number], { kind: 'prop' }>,
2067
2220
  value: unknown
@@ -2099,22 +2252,35 @@ export function createTamaguiCompilerHost(
2099
2252
  branchProps
2100
2253
  )
2101
2254
  : core.mergeProps(defaultProps, branchProps)
2255
+ // the same platform-clause reduction the unconditional props get: a
2256
+ // branch resolves through the full pipeline, so a clause left standing
2257
+ // here would be decided by the build machine instead of the bundle
2258
+ if (platform === 'native') {
2259
+ for (const [name, value] of Object.entries(branchCompleteProps)) {
2260
+ const clause = isStyleProp(name, component) ? nativeClauseValue(value) : null
2261
+ if (clause === null || clause === 'live') continue
2262
+ if (clause.matched) branchCompleteProps[name] = clause.payload
2263
+ else delete branchCompleteProps[name]
2264
+ }
2265
+ }
2102
2266
  return { branchProps, branchCompleteProps }
2103
2267
  }
2104
- // Against completeProps, not props: clauses also arrive from styled()
2105
- // defaults. Native resolution evaluates them against the build machine's
2106
- // current state, so folding would freeze that state into the bundle.
2268
+ // native resolution evaluates clauses against the build machine's current
2269
+ // state, so folding a call-site, styled base, or variant clause would freeze
2270
+ // that state into the bundle.
2107
2271
  if (platform === 'native') {
2108
2272
  const isClauseValue = (name: string, value: unknown) =>
2109
2273
  isStyleProp(name, component) &&
2110
2274
  ((typeof value === 'string' && flatClausePattern.test(value)) ||
2111
2275
  isClauseObjectValue(value))
2112
2276
  // a clause can also sit inside a variant definition, where it never
2113
- // appears as a prop: variants: { big: { true: { width: 'gt-lg:999px' } } }
2277
+ // appears as a prop: variants: { big: { true: { width: 'gt-lg:999px' } } }.
2278
+ // those live in the styled definition, which this element cannot rewrite,
2279
+ // so a clause there retains whatever kind it is.
2114
2280
  const defaultVariants = component.staticConfig.defaultVariants ?? {}
2115
- const carriesClause =
2116
- Object.entries(completeProps).some(([name, value]) =>
2117
- isClauseValue(name, value)
2281
+ const definitionCarriesClause =
2282
+ Object.entries(resolvedStyleStaticConfig.baseStyle ?? {}).some(
2283
+ ([name, value]) => isClauseValue(name, value)
2118
2284
  ) ||
2119
2285
  Object.entries(component.staticConfig.variants ?? {}).some(
2120
2286
  ([variantName, definitions]) =>
@@ -2129,13 +2295,43 @@ export function createTamaguiCompilerHost(
2129
2295
  )
2130
2296
  )
2131
2297
  )
2132
- if (carriesClause) {
2298
+ if (definitionCarriesClause) {
2133
2299
  return bailout(
2134
2300
  input,
2135
2301
  'local/unsupported-target',
2136
2302
  'Native conditional value programs remain on the runtime path'
2137
2303
  )
2138
2304
  }
2305
+ const reducedProps: Record<string, unknown> = {}
2306
+ for (const [name, value] of Object.entries(completeProps)) {
2307
+ const clause = isStyleProp(name, component) ? nativeClauseValue(value) : null
2308
+ if (clause === 'live') {
2309
+ return bailout(
2310
+ input,
2311
+ 'local/unsupported-target',
2312
+ 'Native conditional value programs remain on the runtime path'
2313
+ )
2314
+ }
2315
+ if (clause === null) {
2316
+ reducedProps[name] = value
2317
+ continue
2318
+ }
2319
+ if (clause.matched) reducedProps[name] = clause.payload
2320
+ }
2321
+ completeProps = reducedProps
2322
+ // a conditional leaf is a clause program the same way a direct prop is,
2323
+ // and propsForConditional reduces the ones that fold
2324
+ for (const entry of dynamicStyleEntries) {
2325
+ if (entry.value.kind !== 'conditional') continue
2326
+ for (const leaf of collectLeaves(entry.value.tree)) {
2327
+ if (nativeClauseValue(leaf.value) !== 'live') continue
2328
+ return bailout(
2329
+ input,
2330
+ 'local/unsupported-target',
2331
+ 'Native conditional value programs remain on the runtime path'
2332
+ )
2333
+ }
2334
+ }
2139
2335
  }
2140
2336
  const split = resolveSplitStyles(
2141
2337
  completeProps,
@@ -2167,13 +2363,6 @@ export function createTamaguiCompilerHost(
2167
2363
  }
2168
2364
  )
2169
2365
  }
2170
- const domStyleProgram = input.element.entries.find(
2171
- (entry) =>
2172
- entry.kind === 'prop' &&
2173
- entry.name === 'style' &&
2174
- entry.value.kind === 'dom-style'
2175
- )
2176
-
2177
2366
  const flatTag =
2178
2367
  component.domTag ??
2179
2368
  (typeof props.render === 'string'
@@ -2193,7 +2382,9 @@ export function createTamaguiCompilerHost(
2193
2382
  }))
2194
2383
 
2195
2384
  const isPropIgnored = (name: string) =>
2196
- isStyleProp(name, component) || isInvalidHostStyleProp(name, component)
2385
+ isStyleProp(name, component) ||
2386
+ isInvalidHostStyleProp(name, component) ||
2387
+ componentOnlyProps.has(name)
2197
2388
  const spreadReplacement = (
2198
2389
  form: MaterializedElement['form'],
2199
2390
  entry: MaterializedElement['entries'][number]
@@ -2210,16 +2401,11 @@ export function createTamaguiCompilerHost(
2210
2401
 
2211
2402
  let styleEntries = input.element.entries.filter(
2212
2403
  (entry) =>
2213
- (entry.kind === 'prop' &&
2214
- (isStyleProp(entry.name, component) ||
2215
- isInvalidHostStyleProp(entry.name, component))) ||
2404
+ (entry.kind === 'prop' && isPropIgnored(entry.name)) ||
2216
2405
  (entry.kind === 'spread' &&
2217
2406
  entry.value.kind === 'static' &&
2218
2407
  staticObject(entry.value.value) &&
2219
- Object.keys(entry.value.value).some(
2220
- (name) =>
2221
- isStyleProp(name, component) || isInvalidHostStyleProp(name, component)
2222
- ))
2408
+ Object.keys(entry.value.value).some(isPropIgnored))
2223
2409
  )
2224
2410
  let invalidHostStyle:
2225
2411
  | { entry: MaterializedElement['entries'][number]; name: string }
@@ -3361,7 +3547,8 @@ function bailout(
3361
3547
  | 'local/unsupported-child',
3362
3548
  message: string,
3363
3549
  span = input.element.span,
3364
- zero?: { rule: ZeroRule; message?: string }
3550
+ zero?: { rule: ZeroRule; message?: string },
3551
+ prop?: string
3365
3552
  ): LoweringCandidateResult {
3366
3553
  return {
3367
3554
  ok: false,
@@ -3371,6 +3558,7 @@ function bailout(
3371
3558
  message,
3372
3559
  span,
3373
3560
  component: input.element.component.name,
3561
+ ...(prop && { prop }),
3374
3562
  ...(zero && { zeroRule: zero.rule, zeroMessage: zero.message }),
3375
3563
  },
3376
3564
  }
@@ -0,0 +1,105 @@
1
+ import type { MaterializedModule } from '@tamagui/compiler-core'
2
+ import path from 'node:path'
3
+
4
+ import type { CompilerComponentRegistry } from './compilerHost'
5
+ import {
6
+ getComponentStaticConfigByName,
7
+ type LoadedComponents,
8
+ } from './extractor/bundleConfig'
9
+
10
+ /**
11
+ * Evaluate a host-resolved module and return its exports, or null when the
12
+ * host cannot run it. Frontends supply this from their bundler's module runner
13
+ * (Vite) or the static-evaluation require hooks (`evaluateComponentModule`).
14
+ */
15
+ export type ComponentModuleEvaluator = (module: {
16
+ id: string
17
+ specifier: string
18
+ }) => Promise<Record<string, unknown> | null>
19
+
20
+ // `pkg`, `@scope/pkg`, and their subpaths. `@/x` and `~/x` are path aliases
21
+ const packageSpecifier = /^(?:@[^/@\s]+\/)?[^./~@\s][^/\s]*(?:\/|$)/
22
+
23
+ function isPackageSpecifier(specifier: string): boolean {
24
+ return packageSpecifier.test(specifier)
25
+ }
26
+
27
+ /**
28
+ * Finds the component modules a file uses that the configured `components`
29
+ * list does not cover, evaluates each once, and registers what it exports so
30
+ * lowering resolves them like configured ones. Owned by a frontend for the
31
+ * life of a project generation; `prepare` runs between materialize and lower.
32
+ */
33
+ export class ComponentDiscovery {
34
+ /** null records a module that had no components or could not evaluate */
35
+ readonly #discovered = new Map<string, LoadedComponents | null>()
36
+
37
+ clear(): void {
38
+ this.#discovered.clear()
39
+ }
40
+
41
+ /** host-resolved ids of every module discovery found components in */
42
+ ids(): string[] {
43
+ return [...this.#discovered]
44
+ .filter(([, loaded]) => loaded !== null)
45
+ .map(([id]) => id)
46
+ .sort()
47
+ }
48
+
49
+ /** re-register everything found so far into a freshly built registry */
50
+ seed(registry: CompilerComponentRegistry): void {
51
+ for (const [id, loaded] of this.#discovered) {
52
+ if (!loaded) continue
53
+ registry.modulesById.set(id, id)
54
+ registry.componentsByModule.set(id, loaded)
55
+ }
56
+ }
57
+
58
+ async prepare(
59
+ module: MaterializedModule,
60
+ registry: CompilerComponentRegistry,
61
+ evaluate: ComponentModuleEvaluator | undefined
62
+ ): Promise<void> {
63
+ if (!evaluate) return
64
+ const seen = new Set<string>()
65
+ const provenances = [
66
+ ...module.elements.map((element) => element.component.provenance),
67
+ ...module.styledDefinitions.map((definition) => definition.base.provenance),
68
+ ]
69
+ for (const provenance of provenances) {
70
+ // a relative or aliased import is the app's own module: the graph
71
+ // already models its styled definitions, and evaluating app code at
72
+ // build time runs its side effects
73
+ if (!provenance || !isPackageSpecifier(provenance.specifier)) continue
74
+ const id = provenance.resolvedId.split(/[?#]/, 1)[0]
75
+ if (seen.has(id) || registry.modulesById.has(id) || this.#discovered.has(id)) {
76
+ continue
77
+ }
78
+ seen.add(id)
79
+ if (!path.isAbsolute(id)) {
80
+ this.#discovered.set(id, null)
81
+ continue
82
+ }
83
+ let loaded: LoadedComponents | null = null
84
+ try {
85
+ const exports = await evaluate({ id, specifier: provenance.specifier })
86
+ if (exports) {
87
+ const nameToInfo = getComponentStaticConfigByName(
88
+ id,
89
+ (exports as { default?: unknown }).default ?? exports
90
+ )
91
+ if (Object.keys(nameToInfo).length) loaded = { moduleName: id, nameToInfo }
92
+ }
93
+ } catch (error) {
94
+ if (process.env.DEBUG === 'tamagui') {
95
+ console.info(`[tamagui] component discovery skipped ${id}:`, error)
96
+ }
97
+ }
98
+ this.#discovered.set(id, loaded)
99
+ if (loaded) {
100
+ registry.modulesById.set(id, id)
101
+ registry.componentsByModule.set(id, loaded)
102
+ }
103
+ }
104
+ }
105
+ }