@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
package/src/check.ts ADDED
@@ -0,0 +1,204 @@
1
+ // The project checker behind `tamagui check`.
2
+ //
3
+ // Node-only entry: walks a project's source files, extracts every static flat
4
+ // value site with the sucrase tokenizer, and reports the same diagnostics the
5
+ // editor plugin and eslint rule produce, formatted as readable code frames.
6
+
7
+ import { readdirSync, readFileSync } from 'node:fs'
8
+ import { join, relative } from 'node:path'
9
+ // deep paths, spelled down to the file: sucrase publishes no `exports` map, and
10
+ // a bare `sucrase/dist/parser` is a directory import that ESM cannot resolve
11
+ import { parse } from 'sucrase/dist/parser/index.js'
12
+ import { TokenType } from 'sucrase/dist/parser/tokenizer/types.js'
13
+
14
+ import { createStyleTooling, type SerializedConfigFile } from './core'
15
+ import {
16
+ createDocumentStyleTooling,
17
+ type DocumentDiagnostic,
18
+ type ExtractStyleSites,
19
+ } from './document'
20
+ import { createSucraseStyleSiteExtractor } from './extract-sucrase'
21
+
22
+ const sourceExtensions = /\.(tsx|jsx)$/
23
+ const skippedDirectories = new Set([
24
+ 'node_modules',
25
+ 'dist',
26
+ 'build',
27
+ 'out',
28
+ 'coverage',
29
+ '.git',
30
+ '.next',
31
+ '.expo',
32
+ '.tamagui',
33
+ ])
34
+
35
+ export interface CheckStyleFilesOptions {
36
+ /** project root; file discovery and relative display paths anchor here */
37
+ root: string
38
+ /** path to the compiler's config artifact; default `<root>/.tamagui/tamagui.config.json` */
39
+ configPath?: string
40
+ /** explicit files to check instead of walking the root */
41
+ files?: readonly string[]
42
+ }
43
+
44
+ export interface CheckedFile {
45
+ /** root-relative display path */
46
+ file: string
47
+ source: string
48
+ diagnostics: readonly DocumentDiagnostic[]
49
+ }
50
+
51
+ export interface CheckStyleFilesResult {
52
+ files: readonly CheckedFile[]
53
+ checkedFileCount: number
54
+ diagnosticCount: number
55
+ }
56
+
57
+ export class MissingConfigArtifactError extends Error {
58
+ constructor(configPath: string) {
59
+ super(
60
+ `no Tamagui config artifact at ${configPath} — run your dev server or \`tamagui generate\` once so the compiler emits it`
61
+ )
62
+ this.name = 'MissingConfigArtifactError'
63
+ }
64
+ }
65
+
66
+ function walkSourceFiles(directory: string, results: string[]): void {
67
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
68
+ if (entry.isDirectory()) {
69
+ if (!skippedDirectories.has(entry.name) && !entry.name.startsWith('.')) {
70
+ walkSourceFiles(join(directory, entry.name), results)
71
+ }
72
+ continue
73
+ }
74
+ if (sourceExtensions.test(entry.name)) results.push(join(directory, entry.name))
75
+ }
76
+ }
77
+
78
+ export function createProjectExtractor(
79
+ isStyleProp: (name: string) => boolean
80
+ ): ExtractStyleSites {
81
+ return createSucraseStyleSiteExtractor({ parse, TokenType }, { isStyleProp })
82
+ }
83
+
84
+ export function checkStyleFiles(options: CheckStyleFilesOptions): CheckStyleFilesResult {
85
+ const configPath =
86
+ options.configPath ?? join(options.root, '.tamagui', 'tamagui.config.json')
87
+ let contents: string
88
+ try {
89
+ contents = readFileSync(configPath, 'utf8')
90
+ } catch {
91
+ throw new MissingConfigArtifactError(configPath)
92
+ }
93
+ const tooling = createStyleTooling(JSON.parse(contents) as SerializedConfigFile)
94
+ if (!tooling) throw new MissingConfigArtifactError(configPath)
95
+
96
+ const document = createDocumentStyleTooling(
97
+ tooling,
98
+ createProjectExtractor((name) => tooling.isStyleProp(name))
99
+ )
100
+
101
+ let files: string[]
102
+ if (options.files) {
103
+ files = [...options.files]
104
+ } else {
105
+ files = []
106
+ walkSourceFiles(options.root, files)
107
+ files.sort()
108
+ }
109
+
110
+ const checked: CheckedFile[] = []
111
+ let diagnosticCount = 0
112
+ for (const file of files) {
113
+ let source: string
114
+ try {
115
+ source = readFileSync(file, 'utf8')
116
+ } catch {
117
+ continue
118
+ }
119
+ let diagnostics: readonly DocumentDiagnostic[]
120
+ try {
121
+ diagnostics = document.diagnostics(source)
122
+ } catch {
123
+ // a file sucrase cannot parse is a syntax error the real compiler will
124
+ // report; the style checker stays quiet about it
125
+ continue
126
+ }
127
+ if (diagnostics.length === 0) continue
128
+ diagnosticCount += diagnostics.length
129
+ checked.push({ file: relative(options.root, file), source, diagnostics })
130
+ }
131
+
132
+ return { files: checked, checkedFileCount: files.length, diagnosticCount }
133
+ }
134
+
135
+ const ansi = {
136
+ red: (text: string) => `\x1b[31m${text}\x1b[39m`,
137
+ dim: (text: string) => `\x1b[2m${text}\x1b[22m`,
138
+ bold: (text: string) => `\x1b[1m${text}\x1b[22m`,
139
+ cyan: (text: string) => `\x1b[36m${text}\x1b[39m`,
140
+ }
141
+ const plain = {
142
+ red: (text: string) => text,
143
+ dim: (text: string) => text,
144
+ bold: (text: string) => text,
145
+ cyan: (text: string) => text,
146
+ }
147
+
148
+ function lineStarts(source: string): number[] {
149
+ const starts = [0]
150
+ for (let index = 0; index < source.length; index++) {
151
+ if (source.charCodeAt(index) === 10) starts.push(index + 1)
152
+ }
153
+ return starts
154
+ }
155
+
156
+ function positionOf(starts: number[], offset: number): { line: number; column: number } {
157
+ let low = 0
158
+ let high = starts.length - 1
159
+ while (low < high) {
160
+ const mid = (low + high + 1) >> 1
161
+ if (starts[mid] <= offset) low = mid
162
+ else high = mid - 1
163
+ }
164
+ return { line: low, column: offset - starts[low] }
165
+ }
166
+
167
+ /** human-readable report: one code frame per diagnostic, caret-underlined */
168
+ export function formatCheckResults(
169
+ result: CheckStyleFilesResult,
170
+ options: { color?: boolean } = {}
171
+ ): string {
172
+ const paint = options.color === false ? plain : ansi
173
+ const output: string[] = []
174
+
175
+ for (const checked of result.files) {
176
+ const starts = lineStarts(checked.source)
177
+ const lines = checked.source.split('\n')
178
+ for (const diagnostic of checked.diagnostics) {
179
+ const start = positionOf(starts, diagnostic.start)
180
+ const end = positionOf(starts, diagnostic.end)
181
+ output.push(
182
+ `${paint.bold(checked.file)}${paint.dim(`:${start.line + 1}:${start.column + 1}`)} ${paint.red('error')} ${diagnostic.message}`
183
+ )
184
+ const line = lines[start.line] ?? ''
185
+ const gutter = String(start.line + 1)
186
+ output.push(` ${paint.dim(`${gutter} │`)} ${line}`)
187
+ const underlineLength =
188
+ end.line === start.line
189
+ ? Math.max(1, end.column - start.column)
190
+ : Math.max(1, line.length - start.column)
191
+ output.push(
192
+ ` ${paint.dim(`${' '.repeat(gutter.length)} │`)} ${' '.repeat(start.column)}${paint.red('^'.repeat(underlineLength))}`
193
+ )
194
+ output.push('')
195
+ }
196
+ }
197
+
198
+ const summary =
199
+ result.diagnosticCount === 0
200
+ ? `${paint.bold('✓')} ${result.checkedFileCount} files, no flat value problems`
201
+ : `${paint.red(paint.bold(`✗ ${result.diagnosticCount} problem${result.diagnosticCount === 1 ? '' : 's'}`))} in ${result.files.length} file${result.files.length === 1 ? '' : 's'} ${paint.dim(`(${result.checkedFileCount} checked)`)}`
202
+ output.push(summary)
203
+ return output.join('\n')
204
+ }
package/src/core.ts ADDED
@@ -0,0 +1,355 @@
1
+ // The standalone Tamagui style tooling core.
2
+ //
3
+ // Browser-safe by contract: no node imports, no typescript imports. Everything
4
+ // here works from the serialized config JSON the Tamagui compiler emits
5
+ // (`.tamagui/tamagui.config.json`), so the same engine powers the tsserver
6
+ // plugin, the VS Code extension, the CLI checker, and in-browser IDEs.
7
+ //
8
+ // The @tamagui/style-grammar engine owns candidate meaning; this module adds
9
+ // the two things the engine's config view deliberately drops — token and theme
10
+ // VALUES — and projects everything into editor-shaped results: completions,
11
+ // diagnostics, hover, and color swatches.
12
+
13
+ import { normalizeCSSColor, rgba } from '@tamagui/normalize-css-color'
14
+ import {
15
+ annotateStyleValue,
16
+ completeStyleValueAtCursor,
17
+ createCandidatePropertyVocabulary,
18
+ createGrammarConfigViewFromSerializedConfig,
19
+ createModifierRegistry,
20
+ createStylePropSet,
21
+ diagnoseStyleValueProgram,
22
+ programEligibility,
23
+ stateModifierNames,
24
+ type DiagnoseStyleValueOptions,
25
+ type ModifierKind,
26
+ type SerializedGrammarSourceConfig,
27
+ type StyleValueAnnotation,
28
+ type StyleValueCompletion,
29
+ type StyleValueCursorCompletions,
30
+ type StyleValueDiagnostic,
31
+ } from '@tamagui/style-grammar/tooling'
32
+
33
+ export type {
34
+ StyleValueAnnotation,
35
+ StyleValueCompletion,
36
+ StyleValueCursorCompletions,
37
+ StyleValueDiagnostic,
38
+ }
39
+
40
+ /** the shape of the JSON artifact the Tamagui compiler writes */
41
+ export interface SerializedConfigFile {
42
+ tamaguiConfig?: SerializedGrammarSourceConfig
43
+ tamaguiConfigMetadata?: unknown
44
+ }
45
+
46
+ export interface RgbaColor {
47
+ r: number
48
+ g: number
49
+ b: number
50
+ a: number
51
+ }
52
+
53
+ export interface StyleValueColor {
54
+ /** character span within the authored value */
55
+ start: number
56
+ end: number
57
+ text: string
58
+ color: RgbaColor
59
+ /** the theme the preview value came from, when theme-resolved */
60
+ theme?: string
61
+ }
62
+
63
+ export interface StyleValueHover {
64
+ /** character span within the authored value */
65
+ start: number
66
+ end: number
67
+ text: string
68
+ /** markdown suitable for an editor hover */
69
+ markdown: string
70
+ /** a representative color for colorish targets */
71
+ color?: RgbaColor
72
+ }
73
+
74
+ const modifierKindLabel: Readonly<Record<ModifierKind, string>> = {
75
+ state: 'state modifier',
76
+ media: 'media modifier',
77
+ theme: 'theme modifier',
78
+ platform: 'platform modifier',
79
+ group: 'group modifier',
80
+ container: 'container modifier',
81
+ }
82
+
83
+ const modifierKindSort: Readonly<Record<ModifierKind, string>> = {
84
+ state: '00',
85
+ group: '01',
86
+ media: '02',
87
+ container: '03',
88
+ theme: '04',
89
+ platform: '05',
90
+ }
91
+
92
+ const stateModifierSort = new Map(
93
+ stateModifierNames.map((name, index) => [name, index] as const)
94
+ )
95
+
96
+ /**
97
+ * The one sort key every host uses for completion entries, so ordering does
98
+ * not drift between tsserver, LSP, and in-browser consumers: states first in
99
+ * interaction order, then groups, media, containers, themes, platforms, then
100
+ * configured values, then keywords.
101
+ */
102
+ export function completionSortText(
103
+ completion: StyleValueCompletion,
104
+ modifierKind: ModifierKind | undefined
105
+ ): string {
106
+ if (modifierKind) {
107
+ const stateOrder =
108
+ modifierKind === 'state'
109
+ ? `${String(stateModifierSort.get(completion.value) ?? 999).padStart(3, '0')}:`
110
+ : ''
111
+ return `${modifierKindSort[modifierKind]}:${stateOrder}${completion.value}`
112
+ }
113
+ return `${completion.kind === 'configured' ? '10' : '11'}:${completion.value}`
114
+ }
115
+
116
+ /** unwraps a compiler-serialized token variable to its raw value */
117
+ function tokenValue(entry: unknown): unknown {
118
+ if (entry && typeof entry === 'object' && 'val' in entry) {
119
+ return (entry as { val: unknown }).val
120
+ }
121
+ return entry
122
+ }
123
+
124
+ function parseColor(value: unknown, opacity?: number): RgbaColor | null {
125
+ if (typeof value !== 'string') return null
126
+ const normalized = normalizeCSSColor(value)
127
+ if (normalized === null) return null
128
+ const color = rgba(normalized)
129
+ if (opacity !== undefined) color.a = color.a * (opacity / 100)
130
+ return color
131
+ }
132
+
133
+ export interface StyleTooling {
134
+ /** every prop name treated as a flat style value site */
135
+ styleProps: ReadonlySet<string>
136
+ /** engine options, for hosts that call @tamagui/style-grammar directly */
137
+ engine: DiagnoseStyleValueOptions
138
+ isStyleProp(name: string): boolean
139
+ /** the longhand a prop resolves to through the configured shorthands */
140
+ targetProperty(name: string): string
141
+ /**
142
+ * cursor completions for one value slot, or null when the property takes no
143
+ * flat program (unknown props, legacy part props)
144
+ */
145
+ completions(
146
+ property: string,
147
+ value: string,
148
+ cursor: number
149
+ ): StyleValueCursorCompletions | null
150
+ /** the complete static verdict for one authored value */
151
+ diagnostics(property: string, value: string): readonly StyleValueDiagnostic[]
152
+ /** classified spans: modifiers, tokens, keywords, literals */
153
+ annotations(property: string, value: string): readonly StyleValueAnnotation[]
154
+ /** hover content for the annotation under `offset`, or null */
155
+ hover(property: string, value: string, offset: number): StyleValueHover | null
156
+ /** every span that resolves to a presentable color */
157
+ colors(property: string, value: string): readonly StyleValueColor[]
158
+ /** the modifier kind of a registered modifier name */
159
+ modifierKind(name: string): ModifierKind | undefined
160
+ /** root theme names, preview themes first */
161
+ previewThemes: readonly string[]
162
+ }
163
+
164
+ export function createStyleTooling(file: SerializedConfigFile): StyleTooling | null {
165
+ const serialized = file.tamaguiConfig
166
+ if (!serialized) return null
167
+
168
+ const config = createGrammarConfigViewFromSerializedConfig(
169
+ serialized,
170
+ file.tamaguiConfigMetadata
171
+ )
172
+ const registry = createModifierRegistry(config).registry
173
+ const candidates = createCandidatePropertyVocabulary(config)
174
+ const engine: DiagnoseStyleValueOptions = { config, registry, candidates }
175
+ const styleProps = createStylePropSet(config)
176
+
177
+ // themes and tokens keep their VALUES here; the grammar view keeps names only
178
+ const themes = (serialized.themes || {}) as Readonly<
179
+ Record<string, Readonly<Record<string, unknown>> | undefined>
180
+ >
181
+ const rootThemes = Object.keys(themes).filter((name) => !name.includes('_'))
182
+ const previewThemes = [
183
+ ...['light', 'dark'].filter((name) => rootThemes.includes(name)),
184
+ ...rootThemes.filter((name) => name !== 'light' && name !== 'dark'),
185
+ ]
186
+ const tokens = (serialized.tokens || {}) as Readonly<
187
+ Record<string, Readonly<Record<string, unknown>> | undefined>
188
+ >
189
+ const media = (serialized.media || {}) as Readonly<Record<string, unknown>>
190
+
191
+ const targetProperty = (name: string): string => config.shorthands?.[name] || name
192
+
193
+ const themeValue = (name: string): { theme: string; value: unknown } | null => {
194
+ for (const theme of previewThemes) {
195
+ const value = themes[theme]?.[name]
196
+ if (value !== undefined) return { theme, value }
197
+ }
198
+ return null
199
+ }
200
+
201
+ const annotationColor = (
202
+ annotation: StyleValueAnnotation
203
+ ): { color: RgbaColor; theme?: string } | null => {
204
+ if (annotation.kind === 'token' || annotation.kind === 'identifier') {
205
+ const name =
206
+ annotation.opacity !== undefined
207
+ ? annotation.text.slice(0, annotation.text.lastIndexOf('/'))
208
+ : annotation.text
209
+ if (annotation.tokenCategory === 'color') {
210
+ const themed = themeValue(name)
211
+ if (themed) {
212
+ const color = parseColor(themed.value, annotation.opacity)
213
+ return color ? { color, theme: themed.theme } : null
214
+ }
215
+ const color = parseColor(tokenValue(tokens.color?.[name]), annotation.opacity)
216
+ return color ? { color } : null
217
+ }
218
+ if (annotation.kind === 'identifier') {
219
+ // literal CSS colors: hex, rgb()/hsl() never annotate (functions are
220
+ // skipped), but hex runs and named colors do
221
+ const color = parseColor(name)
222
+ return color ? { color } : null
223
+ }
224
+ return null
225
+ }
226
+ if (annotation.kind === 'keyword' && annotation.text === 'transparent') {
227
+ return { color: { r: 0, g: 0, b: 0, a: 0 } }
228
+ }
229
+ return null
230
+ }
231
+
232
+ const annotations = (property: string, value: string) =>
233
+ annotateStyleValue(property, value, engine)
234
+
235
+ const formatRgba = (color: RgbaColor): string =>
236
+ color.a === 1
237
+ ? `rgb(${color.r}, ${color.g}, ${color.b})`
238
+ : `rgba(${color.r}, ${color.g}, ${color.b}, ${Number(color.a.toFixed(3))})`
239
+
240
+ return {
241
+ styleProps,
242
+ engine,
243
+ previewThemes,
244
+ isStyleProp: (name) => styleProps.has(name),
245
+ targetProperty,
246
+ modifierKind: (name) => registry.get(name),
247
+
248
+ completions(property, value, cursor) {
249
+ const target = targetProperty(property)
250
+ if (programEligibility(target) === 'legacy-part') return null
251
+ if (!styleProps.has(property)) return null
252
+ return completeStyleValueAtCursor(property, value, cursor, engine)
253
+ },
254
+
255
+ diagnostics(property, value) {
256
+ if (!styleProps.has(property)) return []
257
+ return diagnoseStyleValueProgram(property, value, engine)
258
+ },
259
+
260
+ annotations,
261
+
262
+ colors(property, value) {
263
+ if (!styleProps.has(property)) return []
264
+ const results: StyleValueColor[] = []
265
+ for (const annotation of annotations(property, value)) {
266
+ const resolved = annotationColor(annotation)
267
+ if (!resolved) continue
268
+ results.push({
269
+ start: annotation.start,
270
+ end: annotation.end,
271
+ text: annotation.text,
272
+ color: resolved.color,
273
+ ...(resolved.theme !== undefined && { theme: resolved.theme }),
274
+ })
275
+ }
276
+ return results
277
+ },
278
+
279
+ hover(property, value, offset) {
280
+ if (!styleProps.has(property)) return null
281
+ const annotation = annotations(property, value).find(
282
+ (entry) => offset >= entry.start && offset <= entry.end
283
+ )
284
+ if (!annotation) return null
285
+
286
+ const lines: string[] = []
287
+ let color: RgbaColor | undefined
288
+
289
+ if (annotation.kind === 'modifier' && annotation.modifierKind) {
290
+ lines.push(
291
+ `**${annotation.text}** · Tamagui ${modifierKindLabel[annotation.modifierKind]}`
292
+ )
293
+ if (annotation.modifierKind === 'media') {
294
+ const query = media[annotation.text]
295
+ if (query !== undefined) {
296
+ lines.push('```json\n' + JSON.stringify(query) + '\n```')
297
+ }
298
+ }
299
+ } else if (annotation.kind === 'token') {
300
+ const name =
301
+ annotation.opacity !== undefined
302
+ ? annotation.text.slice(0, annotation.text.lastIndexOf('/'))
303
+ : annotation.text
304
+ if (annotation.tokenCategory === 'color') {
305
+ lines.push(`**${name}** · Tamagui color`)
306
+ let shown = 0
307
+ for (const theme of previewThemes) {
308
+ const value = themes[theme]?.[name]
309
+ if (value === undefined || shown >= 4) continue
310
+ shown++
311
+ lines.push(`- ${theme}: \`${String(value)}\``)
312
+ }
313
+ if (shown === 0) {
314
+ const value = tokenValue(tokens.color?.[name])
315
+ if (value !== undefined) lines.push(`- \`${String(value)}\``)
316
+ }
317
+ if (annotation.opacity !== undefined) {
318
+ lines.push(`- opacity: ${annotation.opacity}%`)
319
+ }
320
+ } else {
321
+ const value = tokenValue(tokens[annotation.tokenCategory || '']?.[name])
322
+ lines.push(
323
+ `**${name}** · Tamagui ${annotation.tokenCategory} token` +
324
+ (value !== undefined ? ` = \`${String(value)}\`` : '')
325
+ )
326
+ }
327
+ const resolved = annotationColor(annotation)
328
+ if (resolved) {
329
+ color = resolved.color
330
+ lines.push(`- resolves: \`${formatRgba(resolved.color)}\``)
331
+ }
332
+ } else if (annotation.kind === 'keyword') {
333
+ lines.push(
334
+ `**${annotation.text}** · CSS keyword` +
335
+ (annotation.property ? ` for \`${annotation.property}\`` : '')
336
+ )
337
+ } else {
338
+ const resolved = annotationColor(annotation)
339
+ if (!resolved) return null
340
+ color = resolved.color
341
+ lines.push(
342
+ `**${annotation.text}** · CSS color = \`${formatRgba(resolved.color)}\``
343
+ )
344
+ }
345
+
346
+ return {
347
+ start: annotation.start,
348
+ end: annotation.end,
349
+ text: annotation.text,
350
+ markdown: lines.join('\n\n'),
351
+ ...(color !== undefined && { color }),
352
+ }
353
+ },
354
+ }
355
+ }
@@ -0,0 +1,140 @@
1
+ // Document-level orchestration: an extractor locates the style value sites in
2
+ // one source file, the core answers inside each site, and this module joins
3
+ // the two so every host (browser IDE, LSP wrapper, CLI checker) gets
4
+ // file-offset results from one code path.
5
+ //
6
+ // Extractors are pluggable because hosts already own a parse: the tsserver
7
+ // plugin walks TypeScript's AST, the eslint rule walks ESTree, soot's bundler
8
+ // holds sucrase tokens. Each adapter reduces its tree to the same StyleSite
9
+ // contract; nothing downstream re-parses.
10
+
11
+ import type {
12
+ StyleTooling,
13
+ StyleValueColor,
14
+ StyleValueDiagnostic,
15
+ StyleValueHover,
16
+ } from './core'
17
+ import type { StyleValueCursorCompletions } from './core'
18
+
19
+ /** one static string style value in a source file */
20
+ export interface StyleSite {
21
+ /** the authored prop name (`bg`, `padding`) */
22
+ property: string
23
+ /** the cooked string value */
24
+ value: string
25
+ /** file offset of the value's first character (inside the quotes) */
26
+ start: number
27
+ /** file offset just past the value's last character */
28
+ end: number
29
+ /** how the site was authored, for hosts that filter */
30
+ kind: 'jsx-attribute' | 'styled-property'
31
+ }
32
+
33
+ export type ExtractStyleSites = (
34
+ source: string,
35
+ fileName?: string
36
+ ) => readonly StyleSite[]
37
+
38
+ export interface DocumentDiagnostic extends StyleValueDiagnostic {
39
+ site: StyleSite
40
+ }
41
+
42
+ export interface DocumentColor extends StyleValueColor {
43
+ site: StyleSite
44
+ }
45
+
46
+ export interface DocumentHover extends StyleValueHover {
47
+ site: StyleSite
48
+ }
49
+
50
+ export interface DocumentCompletions extends StyleValueCursorCompletions {
51
+ site: StyleSite
52
+ }
53
+
54
+ export interface DocumentStyleTooling {
55
+ sites(source: string, fileName?: string): readonly StyleSite[]
56
+ /** completions at a file offset, spans mapped to file offsets */
57
+ completionsAt(
58
+ source: string,
59
+ offset: number,
60
+ fileName?: string
61
+ ): DocumentCompletions | null
62
+ /** every diagnostic in the file, spans mapped to file offsets */
63
+ diagnostics(source: string, fileName?: string): readonly DocumentDiagnostic[]
64
+ /** hover at a file offset, span mapped to file offsets */
65
+ hoverAt(source: string, offset: number, fileName?: string): DocumentHover | null
66
+ /** every color swatch in the file, spans mapped to file offsets */
67
+ colors(source: string, fileName?: string): readonly DocumentColor[]
68
+ }
69
+
70
+ export function createDocumentStyleTooling(
71
+ tooling: StyleTooling,
72
+ extract: ExtractStyleSites
73
+ ): DocumentStyleTooling {
74
+ const siteAt = (sites: readonly StyleSite[], offset: number): StyleSite | undefined =>
75
+ sites.find((site) => offset >= site.start && offset <= site.end)
76
+
77
+ return {
78
+ sites: (source, fileName) => extract(source, fileName),
79
+
80
+ completionsAt(source, offset, fileName) {
81
+ const site = siteAt(extract(source, fileName), offset)
82
+ if (!site) return null
83
+ const completions = tooling.completions(
84
+ site.property,
85
+ site.value,
86
+ offset - site.start
87
+ )
88
+ if (!completions) return null
89
+ return {
90
+ ...completions,
91
+ replaceStart: site.start + completions.replaceStart,
92
+ site,
93
+ }
94
+ },
95
+
96
+ diagnostics(source, fileName) {
97
+ const results: DocumentDiagnostic[] = []
98
+ for (const site of extract(source, fileName)) {
99
+ for (const diagnostic of tooling.diagnostics(site.property, site.value)) {
100
+ results.push({
101
+ ...diagnostic,
102
+ index: site.start + diagnostic.index,
103
+ start: site.start + diagnostic.start,
104
+ end: site.start + diagnostic.end,
105
+ site,
106
+ })
107
+ }
108
+ }
109
+ return results
110
+ },
111
+
112
+ hoverAt(source, offset, fileName) {
113
+ const site = siteAt(extract(source, fileName), offset)
114
+ if (!site) return null
115
+ const hover = tooling.hover(site.property, site.value, offset - site.start)
116
+ if (!hover) return null
117
+ return {
118
+ ...hover,
119
+ start: site.start + hover.start,
120
+ end: site.start + hover.end,
121
+ site,
122
+ }
123
+ },
124
+
125
+ colors(source, fileName) {
126
+ const results: DocumentColor[] = []
127
+ for (const site of extract(source, fileName)) {
128
+ for (const color of tooling.colors(site.property, site.value)) {
129
+ results.push({
130
+ ...color,
131
+ start: site.start + color.start,
132
+ end: site.start + color.end,
133
+ site,
134
+ })
135
+ }
136
+ }
137
+ return results
138
+ },
139
+ }
140
+ }