@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.
- package/README.md +140 -1
- package/dist/cjs/check.cjs +173 -0
- package/dist/cjs/check.native.cjs +369 -0
- package/dist/cjs/check.native.js +371 -0
- package/dist/cjs/check.native.js.map +1 -0
- package/dist/cjs/core.cjs +221 -0
- package/dist/cjs/core.native.cjs +305 -0
- package/dist/cjs/core.native.js +307 -0
- package/dist/cjs/core.native.js.map +1 -0
- package/dist/cjs/document.cjs +82 -0
- package/dist/cjs/document.native.cjs +158 -0
- package/dist/cjs/document.native.js +160 -0
- package/dist/cjs/document.native.js.map +1 -0
- package/dist/cjs/extract-estree.cjs +193 -0
- package/dist/cjs/extract-estree.native.cjs +296 -0
- package/dist/cjs/extract-estree.native.js +298 -0
- package/dist/cjs/extract-estree.native.js.map +1 -0
- package/dist/cjs/extract-sucrase.cjs +222 -0
- package/dist/cjs/extract-sucrase.native.cjs +256 -0
- package/dist/cjs/extract-sucrase.native.js +258 -0
- package/dist/cjs/extract-sucrase.native.js.map +1 -0
- package/dist/cjs/host.cjs +38 -0
- package/dist/cjs/host.native.cjs +61 -0
- package/dist/cjs/host.native.js +63 -0
- package/dist/cjs/host.native.js.map +1 -0
- package/dist/cjs/index.cjs +301 -0
- package/dist/cjs/index.native.cjs +414 -0
- package/dist/cjs/index.native.js +416 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/esm/check.mjs +148 -0
- package/dist/esm/check.mjs.map +1 -0
- package/dist/esm/check.native.js +342 -0
- package/dist/esm/check.native.js.map +1 -0
- package/dist/esm/core.mjs +198 -0
- package/dist/esm/core.mjs.map +1 -0
- package/dist/esm/core.native.js +280 -0
- package/dist/esm/core.native.js.map +1 -0
- package/dist/esm/document.mjs +61 -0
- package/dist/esm/document.mjs.map +1 -0
- package/dist/esm/document.native.js +135 -0
- package/dist/esm/document.native.js.map +1 -0
- package/dist/esm/extract-estree.mjs +172 -0
- package/dist/esm/extract-estree.mjs.map +1 -0
- package/dist/esm/extract-estree.native.js +273 -0
- package/dist/esm/extract-estree.native.js.map +1 -0
- package/dist/esm/extract-sucrase.mjs +201 -0
- package/dist/esm/extract-sucrase.mjs.map +1 -0
- package/dist/esm/extract-sucrase.native.js +233 -0
- package/dist/esm/extract-sucrase.native.js.map +1 -0
- package/dist/esm/host.mjs +17 -0
- package/dist/esm/host.mjs.map +1 -0
- package/dist/esm/host.native.js +38 -0
- package/dist/esm/host.native.js.map +1 -0
- package/dist/esm/index.mjs +281 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +392 -0
- package/dist/esm/index.native.js.map +1 -0
- package/index.cjs +1 -0
- package/package.json +79 -7
- package/src/check.ts +204 -0
- package/src/core.ts +355 -0
- package/src/document.ts +140 -0
- package/src/extract-estree.ts +251 -0
- package/src/extract-sucrase.ts +308 -0
- package/src/host.ts +32 -0
- package/src/index.ts +452 -0
- package/types/check.d.ts +31 -0
- package/types/check.d.ts.map +11 -0
- package/types/core.d.ts +68 -0
- package/types/core.d.ts.map +11 -0
- package/types/document.d.ts +42 -0
- package/types/document.d.ts.map +11 -0
- package/types/extract-estree.d.ts +19 -0
- package/types/extract-estree.d.ts.map +11 -0
- package/types/extract-sucrase.d.ts +26 -0
- package/types/extract-sucrase.d.ts.map +11 -0
- package/types/host.d.ts +10 -0
- package/types/host.d.ts.map +11 -0
- package/types/index.d.ts +9 -0
- 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
|
+
}
|