@tamagui/language-service 0.0.0-bootstrap.0 → 3.0.0-beta.643.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +140 -1
  2. package/dist/cjs/check.cjs +173 -0
  3. package/dist/cjs/check.native.cjs +369 -0
  4. package/dist/cjs/check.native.js +371 -0
  5. package/dist/cjs/check.native.js.map +1 -0
  6. package/dist/cjs/core.cjs +221 -0
  7. package/dist/cjs/core.native.cjs +305 -0
  8. package/dist/cjs/core.native.js +307 -0
  9. package/dist/cjs/core.native.js.map +1 -0
  10. package/dist/cjs/document.cjs +82 -0
  11. package/dist/cjs/document.native.cjs +158 -0
  12. package/dist/cjs/document.native.js +160 -0
  13. package/dist/cjs/document.native.js.map +1 -0
  14. package/dist/cjs/extract-estree.cjs +193 -0
  15. package/dist/cjs/extract-estree.native.cjs +296 -0
  16. package/dist/cjs/extract-estree.native.js +298 -0
  17. package/dist/cjs/extract-estree.native.js.map +1 -0
  18. package/dist/cjs/extract-sucrase.cjs +222 -0
  19. package/dist/cjs/extract-sucrase.native.cjs +256 -0
  20. package/dist/cjs/extract-sucrase.native.js +258 -0
  21. package/dist/cjs/extract-sucrase.native.js.map +1 -0
  22. package/dist/cjs/host.cjs +38 -0
  23. package/dist/cjs/host.native.cjs +61 -0
  24. package/dist/cjs/host.native.js +63 -0
  25. package/dist/cjs/host.native.js.map +1 -0
  26. package/dist/cjs/index.cjs +301 -0
  27. package/dist/cjs/index.native.cjs +414 -0
  28. package/dist/cjs/index.native.js +416 -0
  29. package/dist/cjs/index.native.js.map +1 -0
  30. package/dist/esm/check.mjs +148 -0
  31. package/dist/esm/check.mjs.map +1 -0
  32. package/dist/esm/check.native.js +342 -0
  33. package/dist/esm/check.native.js.map +1 -0
  34. package/dist/esm/core.mjs +198 -0
  35. package/dist/esm/core.mjs.map +1 -0
  36. package/dist/esm/core.native.js +280 -0
  37. package/dist/esm/core.native.js.map +1 -0
  38. package/dist/esm/document.mjs +61 -0
  39. package/dist/esm/document.mjs.map +1 -0
  40. package/dist/esm/document.native.js +135 -0
  41. package/dist/esm/document.native.js.map +1 -0
  42. package/dist/esm/extract-estree.mjs +172 -0
  43. package/dist/esm/extract-estree.mjs.map +1 -0
  44. package/dist/esm/extract-estree.native.js +273 -0
  45. package/dist/esm/extract-estree.native.js.map +1 -0
  46. package/dist/esm/extract-sucrase.mjs +201 -0
  47. package/dist/esm/extract-sucrase.mjs.map +1 -0
  48. package/dist/esm/extract-sucrase.native.js +233 -0
  49. package/dist/esm/extract-sucrase.native.js.map +1 -0
  50. package/dist/esm/host.mjs +17 -0
  51. package/dist/esm/host.mjs.map +1 -0
  52. package/dist/esm/host.native.js +38 -0
  53. package/dist/esm/host.native.js.map +1 -0
  54. package/dist/esm/index.mjs +281 -0
  55. package/dist/esm/index.mjs.map +1 -0
  56. package/dist/esm/index.native.js +392 -0
  57. package/dist/esm/index.native.js.map +1 -0
  58. package/index.cjs +1 -0
  59. package/package.json +79 -7
  60. package/src/check.ts +204 -0
  61. package/src/core.ts +355 -0
  62. package/src/document.ts +140 -0
  63. package/src/extract-estree.ts +251 -0
  64. package/src/extract-sucrase.ts +308 -0
  65. package/src/host.ts +32 -0
  66. package/src/index.ts +452 -0
  67. package/types/check.d.ts +31 -0
  68. package/types/check.d.ts.map +11 -0
  69. package/types/core.d.ts +68 -0
  70. package/types/core.d.ts.map +11 -0
  71. package/types/document.d.ts +42 -0
  72. package/types/document.d.ts.map +11 -0
  73. package/types/extract-estree.d.ts +19 -0
  74. package/types/extract-estree.d.ts.map +11 -0
  75. package/types/extract-sucrase.d.ts +26 -0
  76. package/types/extract-sucrase.d.ts.map +11 -0
  77. package/types/host.d.ts +10 -0
  78. package/types/host.d.ts.map +11 -0
  79. package/types/index.d.ts +9 -0
  80. package/types/index.d.ts.map +11 -0
@@ -0,0 +1,251 @@
1
+ // Style site extraction over an ESTree program.
2
+ //
3
+ // One walker serves every ESTree producer: eslint (espree/typescript-eslint),
4
+ // oxc-parser's raw-transfer AST, and anything else emitting the standard
5
+ // shape. Node spans read `range` when present (eslint) and `start`/`end`
6
+ // otherwise (oxc), so the same call works in both worlds.
7
+ //
8
+ // The recognized site shapes match the sucrase extractor exactly:
9
+ // string-valued JSX attributes on tamagui components, and string property
10
+ // values anywhere inside a styled() definition object.
11
+
12
+ import type { StyleSite } from './document'
13
+
14
+ interface EstreeNode {
15
+ type: string
16
+ range?: readonly [number, number]
17
+ start?: number
18
+ end?: number
19
+ [key: string]: unknown
20
+ }
21
+
22
+ export interface EstreeStyleSite extends StyleSite {
23
+ /** the Literal or TemplateLiteral node holding the value, for reporters */
24
+ node: unknown
25
+ }
26
+
27
+ export interface EstreeExtractorOptions {
28
+ /** which prop names produce sites; default: every prop */
29
+ isStyleProp?: (name: string) => boolean
30
+ /**
31
+ * which import sources mark components and `styled` as tamagui's.
32
+ * default: `tamagui`, `tamagui/*`, `@tamagui/*`
33
+ */
34
+ isTamaguiModule?: (source: string) => boolean
35
+ /** treat every capitalized JSX element as a tamagui component */
36
+ allComponents?: boolean
37
+ }
38
+
39
+ function defaultIsTamaguiModule(source: string): boolean {
40
+ return (
41
+ source === 'tamagui' ||
42
+ source.startsWith('tamagui/') ||
43
+ source.startsWith('@tamagui/')
44
+ )
45
+ }
46
+
47
+ function span(node: EstreeNode): readonly [number, number] | null {
48
+ if (node.range) return node.range
49
+ if (typeof node.start === 'number' && typeof node.end === 'number') {
50
+ return [node.start, node.end]
51
+ }
52
+ return null
53
+ }
54
+
55
+ /** cooked string value plus its content span, or null when not static/exact */
56
+ function staticString(
57
+ node: EstreeNode | null | undefined
58
+ ): { value: string; start: number; end: number } | null {
59
+ if (!node) return null
60
+ const at = span(node)
61
+ if (!at) return null
62
+ if (node.type === 'Literal') {
63
+ const value = node.value
64
+ if (typeof value !== 'string') return null
65
+ // escapes cook differently than they read; an approximate replacement
66
+ // span could delete a runtime clause, so exact-raw literals only
67
+ const raw = node.raw
68
+ if (typeof raw === 'string' && raw.slice(1, -1) !== value) return null
69
+ return { value, start: at[0] + 1, end: at[1] - 1 }
70
+ }
71
+ if (node.type === 'TemplateLiteral') {
72
+ const expressions = node.expressions as EstreeNode[] | undefined
73
+ const quasis = node.quasis as
74
+ | Array<{ value?: { cooked?: string | null; raw?: string } }>
75
+ | undefined
76
+ if (expressions?.length !== 0 || quasis?.length !== 1) return null
77
+ const cooked = quasis[0]?.value?.cooked
78
+ if (typeof cooked !== 'string') return null
79
+ if (quasis[0]?.value?.raw !== cooked) return null
80
+ return { value: cooked, start: at[0] + 1, end: at[1] - 1 }
81
+ }
82
+ return null
83
+ }
84
+
85
+ function identifierName(node: EstreeNode | null | undefined): string | null {
86
+ if (!node) return null
87
+ if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
88
+ return typeof node.name === 'string' ? node.name : null
89
+ }
90
+ return null
91
+ }
92
+
93
+ /** the root identifier of a JSX element name (`T` in `<T.Stack.Deep>`) */
94
+ function jsxRootName(name: EstreeNode | null | undefined): string | null {
95
+ let current = name
96
+ while (current?.type === 'JSXMemberExpression') {
97
+ current = current.object as EstreeNode
98
+ }
99
+ return identifierName(current)
100
+ }
101
+
102
+ const skipKeys = new Set([
103
+ 'parent',
104
+ 'loc',
105
+ 'range',
106
+ 'leadingComments',
107
+ 'trailingComments',
108
+ ])
109
+
110
+ function walk(node: EstreeNode, visit: (node: EstreeNode) => void): void {
111
+ visit(node)
112
+ for (const key in node) {
113
+ if (skipKeys.has(key)) continue
114
+ const value = node[key]
115
+ if (Array.isArray(value)) {
116
+ for (const item of value) {
117
+ if (item && typeof item === 'object' && typeof item.type === 'string') {
118
+ walk(item as EstreeNode, visit)
119
+ }
120
+ }
121
+ } else if (
122
+ value &&
123
+ typeof value === 'object' &&
124
+ typeof (value as EstreeNode).type === 'string'
125
+ ) {
126
+ walk(value as EstreeNode, visit)
127
+ }
128
+ }
129
+ }
130
+
131
+ export function extractStyleSitesFromEstree(
132
+ program: unknown,
133
+ options: EstreeExtractorOptions = {}
134
+ ): readonly EstreeStyleSite[] {
135
+ const isStyleProp = options.isStyleProp ?? (() => true)
136
+ const isTamaguiModule = options.isTamaguiModule ?? defaultIsTamaguiModule
137
+ const root = program as EstreeNode
138
+
139
+ // ── pass 1: imported vocabulary ──────────────────────────────────────────
140
+ const styledBindings = new Set<string>()
141
+ const componentNames = new Set<string>()
142
+
143
+ walk(root, (node) => {
144
+ if (node.type === 'ImportDeclaration') {
145
+ const source = (node.source as EstreeNode | undefined)?.value
146
+ if (typeof source !== 'string' || !isTamaguiModule(source)) return
147
+ for (const specifier of (node.specifiers as EstreeNode[]) || []) {
148
+ const local = identifierName(specifier.local as EstreeNode)
149
+ if (!local) continue
150
+ if (specifier.type === 'ImportNamespaceSpecifier') {
151
+ componentNames.add(local)
152
+ continue
153
+ }
154
+ if (specifier.type !== 'ImportSpecifier') continue
155
+ const importedNode = specifier.imported as EstreeNode
156
+ const imported =
157
+ identifierName(importedNode) ??
158
+ (typeof importedNode?.value === 'string' ? importedNode.value : null)
159
+ if (imported === 'styled') styledBindings.add(local)
160
+ else if (imported && /^[A-Z]/.test(imported)) componentNames.add(local)
161
+ }
162
+ return
163
+ }
164
+ if (node.type === 'VariableDeclarator') {
165
+ const name = identifierName(node.id as EstreeNode)
166
+ const init = node.init as EstreeNode | undefined
167
+ if (
168
+ name &&
169
+ init?.type === 'CallExpression' &&
170
+ identifierName(init.callee as EstreeNode) !== null &&
171
+ styledBindings.has(identifierName(init.callee as EstreeNode)!)
172
+ ) {
173
+ componentNames.add(name)
174
+ }
175
+ }
176
+ })
177
+
178
+ // ── pass 2: sites ────────────────────────────────────────────────────────
179
+ const sites: EstreeStyleSite[] = []
180
+
181
+ const pushSite = (
182
+ property: string,
183
+ valueNode: EstreeNode,
184
+ kind: StyleSite['kind']
185
+ ): void => {
186
+ if (!isStyleProp(property)) return
187
+ const content = staticString(valueNode)
188
+ if (!content) return
189
+ sites.push({
190
+ property,
191
+ value: content.value,
192
+ start: content.start,
193
+ end: content.end,
194
+ kind,
195
+ node: valueNode,
196
+ })
197
+ }
198
+
199
+ const visitStyleObject = (object: EstreeNode): void => {
200
+ for (const member of (object.properties as EstreeNode[]) || []) {
201
+ if (member.type !== 'Property') continue
202
+ if (member.computed) continue
203
+ const key = member.key as EstreeNode
204
+ const property =
205
+ identifierName(key) ?? (typeof key?.value === 'string' ? key.value : null)
206
+ const value = member.value as EstreeNode
207
+ if (property && isStyleProp(property)) {
208
+ pushSite(property, value, 'styled-property')
209
+ continue
210
+ }
211
+ if (value?.type === 'ObjectExpression') {
212
+ visitStyleObject(value)
213
+ } else if (value?.type === 'ArrayExpression') {
214
+ for (const element of (value.elements as Array<EstreeNode | null>) || []) {
215
+ if (element?.type === 'ObjectExpression') visitStyleObject(element)
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ walk(root, (node) => {
222
+ if (node.type === 'CallExpression') {
223
+ const callee = identifierName(node.callee as EstreeNode)
224
+ if (!callee || !styledBindings.has(callee)) return
225
+ const definition = (node.arguments as EstreeNode[])?.[1]
226
+ if (definition?.type === 'ObjectExpression') visitStyleObject(definition)
227
+ return
228
+ }
229
+ if (node.type === 'JSXOpeningElement') {
230
+ const componentRoot = jsxRootName(node.name as EstreeNode)
231
+ const accepted = options.allComponents
232
+ ? componentRoot !== null && /^[A-Z]/.test(componentRoot)
233
+ : componentRoot !== null && componentNames.has(componentRoot)
234
+ if (!accepted) return
235
+ for (const attribute of (node.attributes as EstreeNode[]) || []) {
236
+ if (attribute.type !== 'JSXAttribute') continue
237
+ const property = identifierName(attribute.name as EstreeNode)
238
+ if (!property) continue
239
+ const rawValue = attribute.value as EstreeNode | undefined
240
+ const valueNode =
241
+ rawValue?.type === 'JSXExpressionContainer'
242
+ ? (rawValue.expression as EstreeNode)
243
+ : rawValue
244
+ if (valueNode) pushSite(property, valueNode, 'jsx-attribute')
245
+ }
246
+ }
247
+ })
248
+
249
+ sites.sort((a, b) => a.start - b.start)
250
+ return sites
251
+ }
@@ -0,0 +1,308 @@
1
+ // Style site extraction over sucrase's token stream.
2
+ //
3
+ // The ultra-light path for in-browser IDEs: sucrase's tokenizer already
4
+ // resolves the hard ambiguities (JSX vs TS generics, template boundaries), and
5
+ // a token walk over its output is a few kilobytes on top. The parser is
6
+ // injected rather than imported so a host that already bundles sucrase reuses
7
+ // its copy:
8
+ //
9
+ // import { parse } from 'sucrase/dist/parser/index.js'
10
+ // import { TokenType } from 'sucrase/dist/parser/tokenizer/types.js'
11
+ // const extract = createSucraseStyleSiteExtractor({ parse, TokenType })
12
+ //
13
+ // Two site shapes are recognized, matching the eslint rule and the tsserver
14
+ // plugin: string-valued JSX attributes on tamagui components, and string
15
+ // property values anywhere inside a styled() call's definition object
16
+ // (variants nest arbitrarily). Sites whose raw text differs from its cooked
17
+ // value (escapes, entities, template substitutions) are skipped: replacing an
18
+ // approximate span risks deleting a runtime clause.
19
+
20
+ import type { ExtractStyleSites, StyleSite } from './document'
21
+
22
+ export interface SucraseToken {
23
+ type: number
24
+ start: number
25
+ end: number
26
+ }
27
+
28
+ export interface SucraseParser {
29
+ parse(
30
+ code: string,
31
+ isJSXEnabled: boolean,
32
+ isTypeScriptEnabled: boolean,
33
+ isFlowEnabled: boolean
34
+ ): { tokens: SucraseToken[] }
35
+ TokenType: Readonly<Record<string, number>>
36
+ }
37
+
38
+ export interface SucraseExtractorOptions {
39
+ /** which prop names produce sites; default: every prop */
40
+ isStyleProp?: (name: string) => boolean
41
+ /**
42
+ * which import sources mark components and `styled` as tamagui's.
43
+ * default: `tamagui`, `tamagui/*`, `@tamagui/*`
44
+ */
45
+ isTamaguiModule?: (source: string) => boolean
46
+ /** treat every capitalized JSX element as a tamagui component */
47
+ allComponents?: boolean
48
+ }
49
+
50
+ function defaultIsTamaguiModule(source: string): boolean {
51
+ return (
52
+ source === 'tamagui' ||
53
+ source.startsWith('tamagui/') ||
54
+ source.startsWith('@tamagui/')
55
+ )
56
+ }
57
+
58
+ export function createSucraseStyleSiteExtractor(
59
+ parser: SucraseParser,
60
+ options: SucraseExtractorOptions = {}
61
+ ): ExtractStyleSites {
62
+ const { TokenType } = parser
63
+ const T = {
64
+ import: TokenType._import,
65
+ string: TokenType.string,
66
+ name: TokenType.name,
67
+ comma: TokenType.comma,
68
+ colon: TokenType.colon,
69
+ eq: TokenType.eq,
70
+ dot: TokenType.dot,
71
+ star: TokenType.star,
72
+ slash: TokenType.slash,
73
+ braceL: TokenType.braceL,
74
+ braceR: TokenType.braceR,
75
+ dollarBraceL: TokenType.dollarBraceL,
76
+ parenL: TokenType.parenL,
77
+ parenR: TokenType.parenR,
78
+ backQuote: TokenType.backQuote,
79
+ template: TokenType.template,
80
+ jsxTagStart: TokenType.jsxTagStart,
81
+ jsxTagEnd: TokenType.jsxTagEnd,
82
+ jsxName: TokenType.jsxName,
83
+ }
84
+ const isStyleProp = options.isStyleProp ?? (() => true)
85
+ const isTamaguiModule = options.isTamaguiModule ?? defaultIsTamaguiModule
86
+
87
+ return (source) => {
88
+ const tokens = parser.parse(source, true, true, false).tokens
89
+ const sites: StyleSite[] = []
90
+
91
+ const text = (token: SucraseToken): string => source.slice(token.start, token.end)
92
+
93
+ /** cooked content span of a string/template value token run, else null */
94
+ const stringContent = (
95
+ index: number
96
+ ): { start: number; end: number; next: number } | null => {
97
+ const token = tokens[index]
98
+ if (token.type === T.string) {
99
+ const content = source.slice(token.start + 1, token.end - 1)
100
+ // escapes and JSX entities cook differently than they read; skip
101
+ if (content.includes('\\') || content.includes('&')) return null
102
+ return { start: token.start + 1, end: token.end - 1, next: index + 1 }
103
+ }
104
+ if (token.type === T.backQuote) {
105
+ const inner = tokens[index + 1]
106
+ if (inner?.type === T.backQuote) {
107
+ return { start: token.end, end: inner.start, next: index + 2 }
108
+ }
109
+ if (inner?.type === T.template && tokens[index + 2]?.type === T.backQuote) {
110
+ if (text(inner).includes('\\')) return null
111
+ return { start: inner.start, end: inner.end, next: index + 3 }
112
+ }
113
+ }
114
+ return null
115
+ }
116
+
117
+ // ── pass 1: imported vocabulary ────────────────────────────────────────
118
+ const styledBindings = new Set<string>()
119
+ const componentNames = new Set<string>()
120
+
121
+ for (let index = 0; index < tokens.length; index++) {
122
+ if (tokens[index].type !== T.import) continue
123
+ // find the module string: the first string token before the next
124
+ // statement-ish token; import statements are short, scan ahead
125
+ let cursor = index + 1
126
+ const specifiers: Array<{ imported: string; local: string }> = []
127
+ let namespace: string | null = null
128
+ let sawBrace = false
129
+ while (cursor < tokens.length) {
130
+ const token = tokens[cursor]
131
+ if (token.type === T.string) break
132
+ if (token.type === T.braceL) sawBrace = true
133
+ if (token.type === T.star && tokens[cursor + 2]?.type === T.name) {
134
+ namespace = text(tokens[cursor + 2])
135
+ cursor += 2
136
+ }
137
+ if (token.type === T.name && sawBrace) {
138
+ const imported = text(token)
139
+ if (imported !== 'as' && imported !== 'type' && imported !== 'from') {
140
+ let local = imported
141
+ if (
142
+ tokens[cursor + 1]?.type === T.name &&
143
+ text(tokens[cursor + 1]) === 'as' &&
144
+ tokens[cursor + 2]?.type === T.name
145
+ ) {
146
+ local = text(tokens[cursor + 2])
147
+ cursor += 2
148
+ }
149
+ specifiers.push({ imported, local })
150
+ }
151
+ }
152
+ cursor++
153
+ }
154
+ const moduleToken = tokens[cursor]
155
+ if (!moduleToken || moduleToken.type !== T.string) continue
156
+ const moduleSource = source.slice(moduleToken.start + 1, moduleToken.end - 1)
157
+ if (!isTamaguiModule(moduleSource)) continue
158
+ if (namespace) componentNames.add(namespace)
159
+ for (const specifier of specifiers) {
160
+ if (specifier.imported === 'styled') {
161
+ styledBindings.add(specifier.local)
162
+ } else if (/^[A-Z]/.test(specifier.imported)) {
163
+ componentNames.add(specifier.local)
164
+ }
165
+ }
166
+ index = cursor
167
+ }
168
+
169
+ // `const X = styled(...)` marks X as a tamagui component
170
+ for (let index = 0; index + 3 < tokens.length; index++) {
171
+ if (
172
+ tokens[index].type === T.name &&
173
+ tokens[index + 1].type === T.eq &&
174
+ tokens[index + 2].type === T.name &&
175
+ styledBindings.has(text(tokens[index + 2])) &&
176
+ tokens[index + 3].type === T.parenL
177
+ ) {
178
+ componentNames.add(text(tokens[index]))
179
+ }
180
+ }
181
+
182
+ // ── pass 2: sites ──────────────────────────────────────────────────────
183
+ // open JSX tags nest through attribute expressions, so both trackers are
184
+ // stacks maintained by one linear walk over brace/paren depth
185
+ const tagStack: Array<string | null> = []
186
+ const styledStack: Array<{ parenDepth: number; braceDepth: number; arg: number }> = []
187
+ let parenDepth = 0
188
+ let braceDepth = 0
189
+
190
+ const jsxRootAccepted = (name: string | null): boolean => {
191
+ if (!name) return false
192
+ if (options.allComponents) return /^[A-Z]/.test(name)
193
+ return componentNames.has(name)
194
+ }
195
+
196
+ for (let index = 0; index < tokens.length; index++) {
197
+ const token = tokens[index]
198
+
199
+ if (token.type === T.parenL) parenDepth++
200
+ else if (token.type === T.parenR) {
201
+ parenDepth--
202
+ const styledContext = styledStack[styledStack.length - 1]
203
+ if (styledContext && parenDepth < styledContext.parenDepth) styledStack.pop()
204
+ } else if (token.type === T.braceL || token.type === T.dollarBraceL) {
205
+ braceDepth++
206
+ } else if (token.type === T.braceR) braceDepth--
207
+ else if (token.type === T.jsxTagStart) {
208
+ // every tag head — opening `<X`, self-closing, closing `</X` — ends at
209
+ // exactly one jsxTagEnd, so heads push and pop unconditionally.
210
+ // attributes only ever occur inside the innermost head; nesting comes
211
+ // from attribute expressions like render={<Y a="1" />}
212
+ const isClosing = tokens[index + 1]?.type === T.slash
213
+ const root =
214
+ !isClosing && tokens[index + 1]?.type === T.jsxName
215
+ ? text(tokens[index + 1])
216
+ : null
217
+ tagStack.push(root)
218
+ continue
219
+ } else if (token.type === T.jsxTagEnd) {
220
+ tagStack.pop()
221
+ continue
222
+ }
223
+
224
+ // styled(...) call opens a context
225
+ if (
226
+ token.type === T.name &&
227
+ styledBindings.has(text(token)) &&
228
+ tokens[index + 1]?.type === T.parenL
229
+ ) {
230
+ styledStack.push({ parenDepth: parenDepth + 1, braceDepth, arg: 0 })
231
+ continue
232
+ }
233
+
234
+ // commas at call depth separate styled()'s arguments; only the second
235
+ // argument is the definition object
236
+ const styledContext = styledStack[styledStack.length - 1]
237
+ if (
238
+ styledContext &&
239
+ token.type === T.comma &&
240
+ parenDepth === styledContext.parenDepth &&
241
+ braceDepth === styledContext.braceDepth
242
+ ) {
243
+ styledContext.arg++
244
+ continue
245
+ }
246
+
247
+ // styled definition property: `{ key: 'value' }` at any nesting
248
+ if (
249
+ styledContext?.arg === 1 &&
250
+ (token.type === T.name || token.type === T.string) &&
251
+ (tokens[index - 1]?.type === T.braceL || tokens[index - 1]?.type === T.comma) &&
252
+ tokens[index + 1]?.type === T.colon &&
253
+ braceDepth > styledContext.braceDepth
254
+ ) {
255
+ const property =
256
+ token.type === T.string
257
+ ? source.slice(token.start + 1, token.end - 1)
258
+ : text(token)
259
+ const content = stringContent(index + 2)
260
+ if (content && isStyleProp(property)) {
261
+ const after = tokens[content.next]
262
+ if (after?.type === T.comma || after?.type === T.braceR) {
263
+ sites.push({
264
+ property,
265
+ value: source.slice(content.start, content.end),
266
+ start: content.start,
267
+ end: content.end,
268
+ kind: 'styled-property',
269
+ })
270
+ }
271
+ }
272
+ continue
273
+ }
274
+
275
+ // JSX attribute: jsxName not part of the tag name itself
276
+ if (
277
+ token.type === T.jsxName &&
278
+ tokens[index - 1]?.type !== T.jsxTagStart &&
279
+ tokens[index - 1]?.type !== T.dot &&
280
+ tokens[index - 1]?.type !== T.slash &&
281
+ jsxRootAccepted(tagStack[tagStack.length - 1] ?? null)
282
+ ) {
283
+ const property = text(token)
284
+ if (!isStyleProp(property)) continue
285
+ if (tokens[index + 1]?.type !== T.eq) continue
286
+ const valueIndex = index + 2
287
+ let content: { start: number; end: number; next: number } | null = null
288
+ if (tokens[valueIndex]?.type === T.braceL) {
289
+ const inner = stringContent(valueIndex + 1)
290
+ if (inner && tokens[inner.next]?.type === T.braceR) content = inner
291
+ } else {
292
+ content = stringContent(valueIndex)
293
+ }
294
+ if (content) {
295
+ sites.push({
296
+ property,
297
+ value: source.slice(content.start, content.end),
298
+ start: content.start,
299
+ end: content.end,
300
+ kind: 'jsx-attribute',
301
+ })
302
+ }
303
+ }
304
+ }
305
+
306
+ return sites
307
+ }
308
+ }
package/src/host.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { HostView } from '@tamagui/style-grammar/tooling'
2
+ import type ts from 'typescript'
3
+
4
+ /**
5
+ * resolves the style-prop surface TypeScript assigns to a Tamagui component.
6
+ * the staticConfig marker distinguishes Tamagui hosts from unrelated React
7
+ * components, while the call signatures retain styled component prop types.
8
+ */
9
+ export function resolveTamaguiHost(
10
+ checker: ts.TypeChecker,
11
+ component: ts.Node
12
+ ): HostView | undefined {
13
+ const componentType = checker.getTypeAtLocation(component)
14
+ if (!checker.getPropertyOfType(componentType, 'staticConfig')) return undefined
15
+
16
+ const propsTypes: ts.Type[] = []
17
+ for (const signature of componentType.getCallSignatures()) {
18
+ const props = signature.getParameters()[0]
19
+ if (props) propsTypes.push(checker.getTypeOfSymbolAtLocation(props, component))
20
+ }
21
+ if (propsTypes.length === 0) return undefined
22
+
23
+ return {
24
+ displayName: component.getText(),
25
+ accepts: (property) =>
26
+ propsTypes.some(
27
+ (propsType) =>
28
+ checker.getPropertyOfType(checker.getApparentType(propsType), property) !==
29
+ undefined
30
+ ),
31
+ }
32
+ }