@tamagui/static 3.0.2 → 3.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamagui/static",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
4
4
  "gitHead": "a49cc7ea6b93ba384e77a4880ae48ac4a5635c14",
5
5
  "license": "MIT",
6
6
  "source": "src/index.ts",
@@ -45,22 +45,22 @@
45
45
  "clean:build": "tamagui-build clean:build"
46
46
  },
47
47
  "dependencies": {
48
- "@tamagui/animation-helpers": "3.0.2",
49
- "@tamagui/cli-color": "3.0.2",
50
- "@tamagui/compiler-core": "3.0.2",
51
- "@tamagui/config-default": "3.0.2",
52
- "@tamagui/core": "3.0.2",
53
- "@tamagui/dom": "3.0.2",
54
- "@tamagui/fake-react-native": "3.0.2",
55
- "@tamagui/generate-themes": "3.0.2",
56
- "@tamagui/helpers": "3.0.2",
57
- "@tamagui/helpers-node": "3.0.2",
58
- "@tamagui/react-native-web-internals": "3.0.2",
59
- "@tamagui/react-native-web-lite": "3.0.2",
60
- "@tamagui/shorthands": "3.0.2",
61
- "@tamagui/style-grammar": "3.0.2",
62
- "@tamagui/types": "3.0.2",
63
- "@tamagui/web": "3.0.2",
48
+ "@tamagui/animation-helpers": "3.0.4",
49
+ "@tamagui/cli-color": "3.0.4",
50
+ "@tamagui/compiler-core": "3.0.4",
51
+ "@tamagui/config-default": "3.0.4",
52
+ "@tamagui/core": "3.0.4",
53
+ "@tamagui/dom": "3.0.4",
54
+ "@tamagui/fake-react-native": "3.0.4",
55
+ "@tamagui/generate-themes": "3.0.4",
56
+ "@tamagui/helpers": "3.0.4",
57
+ "@tamagui/helpers-node": "3.0.4",
58
+ "@tamagui/react-native-web-internals": "3.0.4",
59
+ "@tamagui/react-native-web-lite": "3.0.4",
60
+ "@tamagui/shorthands": "3.0.4",
61
+ "@tamagui/style-grammar": "3.0.4",
62
+ "@tamagui/types": "3.0.4",
63
+ "@tamagui/web": "3.0.4",
64
64
  "browserslist": "^4.28.1",
65
65
  "check-dependency-version-consistency": "^4.1.0",
66
66
  "esbuild": "0.28.1",
@@ -73,7 +73,7 @@
73
73
  "react-native-web": "~0.21.0"
74
74
  },
75
75
  "devDependencies": {
76
- "@tamagui/build": "3.0.2",
76
+ "@tamagui/build": "3.0.4",
77
77
  "@types/find-root": "^1.1.2",
78
78
  "@types/node": "^22.1.0",
79
79
  "@types/webpack": "^4.41.26",
@@ -94,5 +94,5 @@
94
94
  "url": "git+https://github.com/tamagui/tamagui.git",
95
95
  "directory": "code/compiler/static"
96
96
  },
97
- "releaseSourceCommit": "8d875db3d642488c86973478d85e1f66db49e5bd"
97
+ "releaseSourceCommit": "c92cf0fea7c8cc8c33eda1f2a95670c66846d251"
98
98
  }
@@ -12,9 +12,15 @@ import type {
12
12
  ZeroRule,
13
13
  } from '@tamagui/compiler-core'
14
14
  import {
15
+ childNode,
16
+ childNodes,
15
17
  collectLeaves,
18
+ findAstNode,
19
+ identifierName,
20
+ parseModuleAst,
16
21
  zeroRuleMessage,
17
22
  zeroThemeBoundaryMessage,
23
+ type AstNode,
18
24
  } from '@tamagui/compiler-core'
19
25
  import {
20
26
  StyleObjectIdentifier,
@@ -828,18 +834,199 @@ const VIEW_WRAPPER_PROPS =
828
834
  * Whether an element can render RN's host component directly instead of
829
835
  * <View>. A child is disqualifying because <View> resets TextAncestorContext
830
836
  * for its descendants when it sits inside a <Text>, and no per-element
831
- * analysis can rule that out; with no children nothing can observe it. A
832
- * spread is disqualifying because it can carry the aria props this checks for.
837
+ * analysis can rule that out; with no children nothing can observe it. The
838
+ * native style wrapper performs that reset itself, so children are allowed
839
+ * through it (`resetsTextAncestor`). A spread is disqualifying because it can
840
+ * carry the aria props this checks for.
833
841
  */
834
- function isBareHostView(element: MaterializedElement): boolean {
842
+ function isBareHostView(
843
+ element: MaterializedElement,
844
+ resetsTextAncestor: boolean
845
+ ): boolean {
835
846
  return !element.entries.some(
836
847
  (entry) =>
837
- entry.kind === 'child' ||
848
+ (entry.kind === 'child' && !resetsTextAncestor) ||
838
849
  entry.kind === 'spread' ||
839
850
  (entry.kind === 'prop' && VIEW_WRAPPER_PROPS.test(entry.name))
840
851
  )
841
852
  }
842
853
 
854
+ /**
855
+ * props RN's <Text> remaps, or that make it pressable, selectable or virtual;
856
+ * a <Text> without them hands its host a fixed set the native style wrapper
857
+ * reproduces
858
+ */
859
+ const TEXT_WRAPPER_PROPS =
860
+ /^(aria-|accessib|on[A-Z]|disabled$|id$|nativeID$|numberOfLines$|ellipsizeMode$|selectable$|selectionColor$|suppressHighlighting$|pressRetentionOffset$|role$|allowFontScaling$)/
861
+
862
+ function isBareHostText(element: MaterializedElement): boolean {
863
+ return !element.entries.some(
864
+ (entry) =>
865
+ entry.kind === 'spread' ||
866
+ (entry.kind === 'prop' && TEXT_WRAPPER_PROPS.test(entry.name))
867
+ )
868
+ }
869
+
870
+ /** ancestors that render their children in the calling component's theme scope */
871
+ const SCOPE_PRESERVING_PARENTS: Record<string, readonly string[]> = {
872
+ react: ['Fragment'],
873
+ 'react-native': ['View', 'ScrollView', 'SafeAreaView'],
874
+ '@tamagui/core': ['View', 'Stack', 'XStack', 'YStack', 'ZStack'],
875
+ tamagui: ['View', 'Stack', 'XStack', 'YStack', 'ZStack'],
876
+ }
877
+
878
+ const SCOPE_PROPS = new Set(['theme', 'themeInverse', 'themeShallow', 'componentName'])
879
+
880
+ // every fast-path element of one module asks about the same source
881
+ let inlineModule: {
882
+ source: string
883
+ program: AstNode
884
+ imports: Map<string, string>
885
+ } | null = null
886
+
887
+ function inlineModuleOf(input: LoweringCandidateInput) {
888
+ if (inlineModule?.source === input.source) return inlineModule
889
+ const program = parseModuleAst(input.source, input.id)
890
+ // local name -> 'source#imported'
891
+ const imports = new Map<string, string>()
892
+ for (const statement of childNodes(program, 'body')) {
893
+ if (statement.type !== 'ImportDeclaration' || statement.importKind === 'type')
894
+ continue
895
+ const from = childNode(statement, 'source')?.value
896
+ for (const specifier of childNodes(statement, 'specifiers')) {
897
+ if (specifier.type !== 'ImportSpecifier') continue
898
+ const local = identifierName(childNode(specifier, 'local'))
899
+ const imported = identifierName(childNode(specifier, 'imported'))
900
+ if (local && imported) imports.set(local, `${from}#${imported}`)
901
+ }
902
+ }
903
+ inlineModule = { source: input.source, program, imports }
904
+ return inlineModule
905
+ }
906
+
907
+ function keepsThemeScope(element: AstNode, imports: Map<string, string>): boolean {
908
+ if (element.type === 'JSXFragment') return true
909
+ const opening = childNode(element, 'openingElement')
910
+ if (!opening) return false
911
+ for (const attribute of childNodes(opening, 'attributes')) {
912
+ if (attribute.type !== 'JSXAttribute') return false
913
+ const name = identifierName(childNode(attribute, 'name'))
914
+ if (!name || SCOPE_PROPS.has(name)) return false
915
+ }
916
+ const [from, imported] = (
917
+ imports.get(identifierName(childNode(opening, 'name')) ?? '') ?? ''
918
+ ).split('#')
919
+ return !!SCOPE_PRESERVING_PARENTS[from!]?.includes(imported!)
920
+ }
921
+
922
+ function isComponentFunction(fn: AstNode, parent: AstNode | undefined): boolean {
923
+ const pascal = (name: string | null) => !!name && /^[A-Z]/.test(name)
924
+ if (fn.type === 'FunctionDeclaration')
925
+ return pascal(identifierName(childNode(fn, 'id')))
926
+ if (!parent) return false
927
+ if (parent.type === 'VariableDeclarator') {
928
+ return (
929
+ childNode(parent, 'init') === fn && pascal(identifierName(childNode(parent, 'id')))
930
+ )
931
+ }
932
+ if (parent.type === 'CallExpression' && childNodes(parent, 'arguments')[0] === fn) {
933
+ const callee = childNode(parent, 'callee')
934
+ const name =
935
+ callee?.type === 'MemberExpression'
936
+ ? identifierName(childNode(callee, 'property'))
937
+ : identifierName(callee)
938
+ return name === 'memo' || name === 'forwardRef'
939
+ }
940
+ return false
941
+ }
942
+
943
+ /** `.map(fn)` and `Array.from(x, fn)` call their callback during the caller's render */
944
+ function isRenderCallback(fn: AstNode, parent: AstNode | undefined): boolean {
945
+ if (parent?.type !== 'CallExpression') return false
946
+ const callee = childNode(parent, 'callee')
947
+ if (callee?.type !== 'MemberExpression') return false
948
+ const args = childNodes(parent, 'arguments')
949
+ const method = identifierName(childNode(callee, 'property'))
950
+ if (method === 'map') return args[0] === fn
951
+ return (
952
+ method === 'from' &&
953
+ identifierName(childNode(callee, 'object')) === 'Array' &&
954
+ args[1] === fn
955
+ )
956
+ }
957
+
958
+ const FUNCTION_TYPES = new Set([
959
+ 'FunctionDeclaration',
960
+ 'FunctionExpression',
961
+ 'ArrowFunctionExpression',
962
+ ])
963
+
964
+ /**
965
+ * Whether a fast-path element can render inline in its component's render
966
+ * (`Fast.inline(<Fast />)`), and whether that call needs JSX braces. Only when
967
+ * the component's theme scope is provably the element's own: the element sits
968
+ * in its returned JSX, possibly through conditionals and `.map` callbacks,
969
+ * under nothing but fragments and plain views that never open a theme scope.
970
+ */
971
+ function nativeInlinePlacement(
972
+ input: LoweringCandidateInput
973
+ ): { braces: boolean } | null {
974
+ if (input.element.form !== 'jsx') return null
975
+ const { program, imports } = inlineModuleOf(input)
976
+ const { start, end } = input.element.span
977
+ const path: { node: AstNode; key: string | null }[] = []
978
+ const element = findAstNode(
979
+ program,
980
+ (node, _parent, key) => {
981
+ path.push({ node, key })
982
+ return node.type === 'JSXElement' && node.start === start && node.end === end
983
+ },
984
+ { start, end }
985
+ )
986
+ if (!element) return null
987
+ const parentType = path.at(-2)?.node.type
988
+ const braces = parentType === 'JSXElement' || parentType === 'JSXFragment'
989
+
990
+ let index = path.length - 2
991
+ while (index >= 0) {
992
+ const { node } = path[index]!
993
+ const childKey = path[index + 1]!.key
994
+ switch (node.type) {
995
+ case 'JSXElement':
996
+ case 'JSXFragment':
997
+ if (childKey !== 'children' || !keepsThemeScope(node, imports)) return null
998
+ break
999
+ case 'ConditionalExpression':
1000
+ if (childKey === 'test') return null
1001
+ break
1002
+ case 'JSXExpressionContainer':
1003
+ case 'LogicalExpression':
1004
+ case 'ParenthesizedExpression':
1005
+ case 'ChainExpression':
1006
+ break
1007
+ case 'ReturnStatement':
1008
+ case 'ArrowFunctionExpression': {
1009
+ if (node.type === 'ArrowFunctionExpression' && childKey !== 'body') return null
1010
+ // a return belongs to its nearest enclosing function
1011
+ let fnIndex = index
1012
+ while (fnIndex >= 0 && !FUNCTION_TYPES.has(path[fnIndex]!.node.type)) fnIndex--
1013
+ if (fnIndex < 0) return null
1014
+ const fn = path[fnIndex]!.node
1015
+ const fnParent = path[fnIndex - 1]?.node
1016
+ if (isComponentFunction(fn, fnParent)) return { braces }
1017
+ if (!isRenderCallback(fn, fnParent)) return null
1018
+ // continue from the call, which is an expression in the outer render
1019
+ index = fnIndex - 1
1020
+ break
1021
+ }
1022
+ default:
1023
+ return null
1024
+ }
1025
+ index--
1026
+ }
1027
+ return null
1028
+ }
1029
+
843
1030
  function entrySource(input: LoweringCandidateInput, entry: DOMPropEntry): string {
844
1031
  return entry.value.kind === 'static'
845
1032
  ? JSON.stringify(entry.value.value)
@@ -2833,7 +3020,7 @@ export function createTamaguiCompilerHost(
2833
3020
  !input.element.entries.some((entry) => entry.kind === 'spread')
2834
3021
  const nativeStyleImports = [
2835
3022
  {
2836
- content: `\nfunction ${nativeStyleLocal}() { return ${nativeStyleLocal}._ ?? (${nativeStyleLocal}._ = ${JSON.stringify((usesNativeFastPath ? nativeStyle : collapseNativeShorthands(nativeStyle)) ?? {})}); } ${nativeStyleLocal}();`,
3023
+ content: `\nfunction ${nativeStyleLocal}() { return ${nativeStyleLocal}._ ?? (${nativeStyleLocal}._ = ${JSON.stringify(collapseNativeShorthands(nativeStyle) ?? {})}); } ${nativeStyleLocal}();`,
2837
3024
  origin: input.element.component.span,
2838
3025
  },
2839
3026
  ]
@@ -3126,12 +3313,18 @@ export function createTamaguiCompilerHost(
3126
3313
  // saves a context read plus a React element per view per render.
3127
3314
  // Only when the call site needs nothing the wrapper adds: the aria
3128
3315
  // remapping (visible in the static props) and the TextAncestorContext
3129
- // reset, which only a child can observe. <Text>'s wrapper does far
3130
- // more than <View>'s, so text keeps the wrapper.
3316
+ // reset, which only a child can observe. Text keeps <Text> as the
3317
+ // component, and the native style wrapper renders its host itself
3318
+ // when the call site passes none of the props <Text> remaps.
3131
3319
  const useHostView =
3132
3320
  options.experimentalNativeFastPath &&
3133
3321
  nativeName === 'View' &&
3134
- isBareHostView(input.element)
3322
+ isBareHostView(input.element, !!nativeFastPath)
3323
+ const useHostText =
3324
+ options.experimentalNativeFastPath &&
3325
+ nativeName === 'Text' &&
3326
+ !!nativeFastPath &&
3327
+ isBareHostText(input.element)
3135
3328
  const nativeExport = useHostView ? 'unstable_NativeView' : nativeName
3136
3329
  // the local is named after the BINDING, not the component: a file can
3137
3330
  // hold both kinds at once, and a shared name would emit two different
@@ -3188,9 +3381,26 @@ export function createTamaguiCompilerHost(
3188
3381
  `Compiled ${input.element.form} call has no editable props argument`
3189
3382
  )
3190
3383
  }
3384
+ const inline = nativeInlinePlacement(input)
3385
+ const inlineEdits = inline
3386
+ ? [
3387
+ {
3388
+ start: input.element.span.start,
3389
+ end: input.element.span.start,
3390
+ content: `${inline.braces ? '{' : ''}${fastLocal}.inline(`,
3391
+ origin: input.element.span,
3392
+ },
3393
+ {
3394
+ start: input.element.span.end,
3395
+ end: input.element.span.end,
3396
+ content: `)${inline.braces ? '}' : ''}`,
3397
+ origin: input.element.span,
3398
+ },
3399
+ ]
3400
+ : []
3191
3401
  return {
3192
3402
  ok: true,
3193
- edits: [...tagEdits, ...styleEdits],
3403
+ edits: [...inlineEdits, ...tagEdits, ...styleEdits],
3194
3404
  css: [],
3195
3405
  imports: [
3196
3406
  ...nativeStyleImports,
@@ -3200,7 +3410,7 @@ export function createTamaguiCompilerHost(
3200
3410
  origin: input.element.component.span,
3201
3411
  },
3202
3412
  {
3203
- content: `\nconst ${fastLocal} = require('@tamagui/core')._withNativeStyle(${nativeLocal}, ${nativeStyleSource}, ${nativeFastPath.mappingLocal});`,
3413
+ content: `\nconst ${fastLocal} = require('@tamagui/core')._withNativeStyle(${nativeLocal}, ${nativeStyleSource}, ${nativeFastPath.mappingLocal}${useHostView ? `, 'view'` : useHostText ? `, 'text'` : ''});`,
3204
3414
  origin: input.element.component.span,
3205
3415
  },
3206
3416
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"compilerHost.d.ts","sourceRoot":"","sources":["../src/compilerHost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,oBAAoB,EACpB,cAAc,EASf,MAAM,wBAAwB,CAAA;AAoC/B,OAAO,KAAK,EAAiC,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAExF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAIhE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,+EAA+E;IAC/E,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;CAClD;AAED,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,gBAAgB,EAAE,SAAS,uBAAuB,EAAE,GACnD,yBAAyB,CAS3B;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,cAAc,CAAA;IACtB,aAAa,EAAE,qBAAqB,CAAA;IACpC,UAAU,EAAE,gBAAgB,EAAE,CAAA;IAC9B,gBAAgB,EAAE,uBAAuB,EAAE,CAAA;IAC3C,qFAAqF;IACrF,QAAQ,CAAC,EAAE,yBAAyB,CAAA;IACpC,wEAAwE;IACxE,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAClC,mEAAmE;IACnE,0BAA0B,CAAC,EAAE,OAAO,CAAA;IACpC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AA88BD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,oBAAoB,CA21FtB"}
1
+ {"version":3,"file":"compilerHost.d.ts","sourceRoot":"","sources":["../src/compilerHost.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,oBAAoB,EACpB,cAAc,EASf,MAAM,wBAAwB,CAAA;AA0C/B,OAAO,KAAK,EAAiC,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAExF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAIhE,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,+EAA+E;IAC/E,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,kBAAkB,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;CAClD;AAED,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,gBAAgB,EAAE,SAAS,uBAAuB,EAAE,GACnD,yBAAyB,CAS3B;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,cAAc,CAAA;IACtB,aAAa,EAAE,qBAAqB,CAAA;IACpC,UAAU,EAAE,gBAAgB,EAAE,CAAA;IAC9B,gBAAgB,EAAE,uBAAuB,EAAE,CAAA;IAC3C,qFAAqF;IACrF,QAAQ,CAAC,EAAE,yBAAyB,CAAA;IACpC,wEAAwE;IACxE,wBAAwB,CAAC,EAAE,OAAO,CAAA;IAClC,mEAAmE;IACnE,0BAA0B,CAAC,EAAE,OAAO,CAAA;IACpC;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAmoCD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,oBAAoB,CAk3FtB"}