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