@tamagui/language-service 3.0.0-beta.924.1 → 3.0.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 (63) hide show
  1. package/README.md +25 -134
  2. package/dist/cjs/check.cjs +85 -10
  3. package/dist/cjs/check.native.cjs +138 -22
  4. package/dist/cjs/check.native.js +138 -22
  5. package/dist/cjs/check.native.js.map +1 -1
  6. package/dist/cjs/core.cjs +8 -4
  7. package/dist/cjs/core.native.cjs +10 -9
  8. package/dist/cjs/core.native.js +10 -9
  9. package/dist/cjs/core.native.js.map +1 -1
  10. package/dist/cjs/document.cjs +4 -3
  11. package/dist/cjs/document.native.cjs +5 -3
  12. package/dist/cjs/document.native.js +5 -3
  13. package/dist/cjs/document.native.js.map +1 -1
  14. package/dist/cjs/extract-estree.native.cjs +2 -6
  15. package/dist/cjs/extract-estree.native.js +2 -6
  16. package/dist/cjs/extract-estree.native.js.map +1 -1
  17. package/dist/cjs/index.cjs +5 -284
  18. package/dist/cjs/index.native.cjs +5 -395
  19. package/dist/cjs/index.native.js +5 -395
  20. package/dist/cjs/index.native.js.map +1 -1
  21. package/dist/esm/check.mjs +87 -12
  22. package/dist/esm/check.mjs.map +1 -1
  23. package/dist/esm/check.native.js +140 -24
  24. package/dist/esm/check.native.js.map +1 -1
  25. package/dist/esm/core.mjs +8 -4
  26. package/dist/esm/core.mjs.map +1 -1
  27. package/dist/esm/core.native.js +10 -9
  28. package/dist/esm/core.native.js.map +1 -1
  29. package/dist/esm/document.mjs +4 -3
  30. package/dist/esm/document.mjs.map +1 -1
  31. package/dist/esm/document.native.js +5 -3
  32. package/dist/esm/document.native.js.map +1 -1
  33. package/dist/esm/extract-estree.native.js +2 -6
  34. package/dist/esm/extract-estree.native.js.map +1 -1
  35. package/dist/esm/index.mjs +5 -279
  36. package/dist/esm/index.native.js +5 -390
  37. package/package.json +8 -17
  38. package/src/check.ts +118 -13
  39. package/src/core.ts +25 -6
  40. package/src/document.ts +21 -4
  41. package/src/index.ts +4 -452
  42. package/types/check.d.ts +4 -0
  43. package/types/check.d.ts.map +2 -2
  44. package/types/core.d.ts +7 -2
  45. package/types/core.d.ts.map +2 -2
  46. package/types/document.d.ts +8 -2
  47. package/types/document.d.ts.map +2 -2
  48. package/types/index.d.ts +4 -7
  49. package/types/index.d.ts.map +2 -2
  50. package/dist/cjs/host.cjs +0 -37
  51. package/dist/cjs/host.native.cjs +0 -60
  52. package/dist/cjs/host.native.js +0 -62
  53. package/dist/cjs/host.native.js.map +0 -1
  54. package/dist/esm/host.mjs +0 -17
  55. package/dist/esm/host.mjs.map +0 -1
  56. package/dist/esm/host.native.js +0 -38
  57. package/dist/esm/host.native.js.map +0 -1
  58. package/dist/esm/index.mjs.map +0 -1
  59. package/dist/esm/index.native.js.map +0 -1
  60. package/index.cjs +0 -1
  61. package/src/host.ts +0 -32
  62. package/types/host.d.ts +0 -10
  63. package/types/host.d.ts.map +0 -11
package/src/core.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Browser-safe by contract: no node imports, no typescript imports. Everything
4
4
  // here works from the serialized config JSON the Tamagui compiler emits
5
- // (`.tamagui/tamagui.config.json`), so the same engine powers the tsserver
5
+ // (`.tamagui/tamagui.config.json`), so the same engine powers the language server
6
6
  // plugin, the VS Code extension, the CLI checker, and in-browser IDEs.
7
7
  //
8
8
  // The @tamagui/style-grammar engine owns candidate meaning; this module adds
@@ -148,7 +148,11 @@ export interface StyleTooling {
148
148
  cursor: number
149
149
  ): StyleValueCursorCompletions | null
150
150
  /** the complete static verdict for one authored value */
151
- diagnostics(property: string, value: string): readonly StyleValueDiagnostic[]
151
+ diagnostics(
152
+ property: string,
153
+ value: string,
154
+ options?: { strictPayloads?: boolean }
155
+ ): readonly StyleValueDiagnostic[]
152
156
  /** classified spans: modifiers, tokens, keywords, literals */
153
157
  annotations(property: string, value: string): readonly StyleValueAnnotation[]
154
158
  /** hover content for the annotation under `offset`, or null */
@@ -161,7 +165,14 @@ export interface StyleTooling {
161
165
  previewThemes: readonly string[]
162
166
  }
163
167
 
164
- export function createStyleTooling(file: SerializedConfigFile): StyleTooling | null {
168
+ export interface StyleToolingOptions {
169
+ strictPayloads?: boolean
170
+ }
171
+
172
+ export function createStyleTooling(
173
+ file: SerializedConfigFile,
174
+ toolingOptions?: StyleToolingOptions
175
+ ): StyleTooling | null {
165
176
  const serialized = file.tamaguiConfig
166
177
  if (!serialized) return null
167
178
 
@@ -171,7 +182,12 @@ export function createStyleTooling(file: SerializedConfigFile): StyleTooling | n
171
182
  )
172
183
  const registry = createModifierRegistry(config).registry
173
184
  const candidates = createCandidatePropertyVocabulary(config)
174
- const engine: DiagnoseStyleValueOptions = { config, registry, candidates }
185
+ const engine: DiagnoseStyleValueOptions = {
186
+ config,
187
+ registry,
188
+ candidates,
189
+ strictPayloads: toolingOptions?.strictPayloads,
190
+ }
175
191
  const styleProps = createStylePropSet(config)
176
192
 
177
193
  // themes and tokens keep their VALUES here; the grammar view keeps names only
@@ -252,9 +268,12 @@ export function createStyleTooling(file: SerializedConfigFile): StyleTooling | n
252
268
  return completeStyleValueAtCursor(property, value, cursor, engine)
253
269
  },
254
270
 
255
- diagnostics(property, value) {
271
+ diagnostics(property, value, diagnosticOptions) {
256
272
  if (!styleProps.has(property)) return []
257
- return diagnoseStyleValueProgram(property, value, engine)
273
+ return diagnoseStyleValueProgram(property, value, {
274
+ ...engine,
275
+ strictPayloads: diagnosticOptions?.strictPayloads ?? engine.strictPayloads,
276
+ })
258
277
  },
259
278
 
260
279
  annotations,
package/src/document.ts CHANGED
@@ -51,6 +51,14 @@ export interface DocumentCompletions extends StyleValueCursorCompletions {
51
51
  site: StyleSite
52
52
  }
53
53
 
54
+ export interface DocumentDiagnosticOptions {
55
+ strictPayloads?: boolean
56
+ }
57
+
58
+ export interface DocumentStyleToolingOptions {
59
+ strictPayloads?: boolean
60
+ }
61
+
54
62
  export interface DocumentStyleTooling {
55
63
  sites(source: string, fileName?: string): readonly StyleSite[]
56
64
  /** completions at a file offset, spans mapped to file offsets */
@@ -60,7 +68,11 @@ export interface DocumentStyleTooling {
60
68
  fileName?: string
61
69
  ): DocumentCompletions | null
62
70
  /** every diagnostic in the file, spans mapped to file offsets */
63
- diagnostics(source: string, fileName?: string): readonly DocumentDiagnostic[]
71
+ diagnostics(
72
+ source: string,
73
+ fileName?: string,
74
+ options?: DocumentDiagnosticOptions
75
+ ): readonly DocumentDiagnostic[]
64
76
  /** hover at a file offset, span mapped to file offsets */
65
77
  hoverAt(source: string, offset: number, fileName?: string): DocumentHover | null
66
78
  /** every color swatch in the file, spans mapped to file offsets */
@@ -69,7 +81,8 @@ export interface DocumentStyleTooling {
69
81
 
70
82
  export function createDocumentStyleTooling(
71
83
  tooling: StyleTooling,
72
- extract: ExtractStyleSites
84
+ extract: ExtractStyleSites,
85
+ toolingOptions?: DocumentStyleToolingOptions
73
86
  ): DocumentStyleTooling {
74
87
  const siteAt = (sites: readonly StyleSite[], offset: number): StyleSite | undefined =>
75
88
  sites.find((site) => offset >= site.start && offset <= site.end)
@@ -93,10 +106,14 @@ export function createDocumentStyleTooling(
93
106
  }
94
107
  },
95
108
 
96
- diagnostics(source, fileName) {
109
+ diagnostics(source, fileName, diagnosticOptions) {
97
110
  const results: DocumentDiagnostic[] = []
111
+ const strictPayloads =
112
+ diagnosticOptions?.strictPayloads ?? toolingOptions?.strictPayloads
98
113
  for (const site of extract(source, fileName)) {
99
- for (const diagnostic of tooling.diagnostics(site.property, site.value)) {
114
+ for (const diagnostic of tooling.diagnostics(site.property, site.value, {
115
+ strictPayloads,
116
+ })) {
100
117
  results.push({
101
118
  ...diagnostic,
102
119
  index: site.start + diagnostic.index,
package/src/index.ts CHANGED
@@ -1,452 +1,4 @@
1
- import { isAbsolute, resolve } from 'node:path'
2
-
3
- import type ts from 'typescript'
4
- import {
5
- completionSortText,
6
- createStyleTooling,
7
- type SerializedConfigFile,
8
- type StyleTooling,
9
- } from './core'
10
- import { resolveTamaguiHost } from './host'
11
-
12
- const completionSource = '@tamagui/language-service'
13
- const diagnosticCode = 78711
14
- const defaultConfigPath = '.tamagui/tamagui.config.json'
15
- const flatStyledModules: ReadonlySet<string> = new Set([
16
- 'tamagui',
17
- 'tamagui/unstyled',
18
- '@tamagui/core',
19
- '@tamagui/ui',
20
- '@tamagui/web',
21
- ])
22
-
23
- export interface TamaguiLanguageServicePluginConfig {
24
- /** Path to the config JSON emitted by the Tamagui compiler. */
25
- configPath?: string
26
- }
27
-
28
- type ImportedBindings = {
29
- styled: ReadonlySet<string>
30
- }
31
-
32
- type LiteralSite = {
33
- property: string
34
- /** cooked value, identical to its authored source text */
35
- value: string
36
- /** file offset of the value's first character */
37
- contentStart: number
38
- literal: ts.StringLiteralLike
39
- }
40
-
41
- function loadTooling(
42
- info: ts.server.PluginCreateInfo,
43
- configPath: string
44
- ): StyleTooling | null {
45
- const contents = info.serverHost.readFile(configPath)
46
- if (contents === undefined) return null
47
- try {
48
- return createStyleTooling(JSON.parse(contents) as SerializedConfigFile)
49
- } catch {
50
- return null
51
- }
52
- }
53
-
54
- function findStringLiteralAtPosition(
55
- typescript: typeof ts,
56
- sourceFile: ts.SourceFile,
57
- position: number
58
- ): ts.StringLiteralLike | null {
59
- let found: ts.StringLiteralLike | null = null
60
- const visit = (node: ts.Node): void => {
61
- if (position < node.getFullStart() || position > node.getEnd()) return
62
- if (
63
- typescript.isStringLiteralLike(node) &&
64
- position >= node.getStart(sourceFile) + 1 &&
65
- position <= node.getEnd() - 1
66
- ) {
67
- found = node
68
- return
69
- }
70
- typescript.forEachChild(node, visit)
71
- }
72
- visit(sourceFile)
73
- return found
74
- }
75
-
76
- function importedBindings(
77
- typescript: typeof ts,
78
- sourceFile: ts.SourceFile
79
- ): ImportedBindings {
80
- const styled = new Set<string>()
81
-
82
- for (const statement of sourceFile.statements) {
83
- if (
84
- !typescript.isImportDeclaration(statement) ||
85
- !typescript.isStringLiteral(statement.moduleSpecifier)
86
- ) {
87
- continue
88
- }
89
- const source = statement.moduleSpecifier.text
90
- if (!flatStyledModules.has(source)) continue
91
- const bindings = statement.importClause?.namedBindings
92
- if (!bindings) continue
93
- if (typescript.isNamespaceImport(bindings)) continue
94
- for (const element of bindings.elements) {
95
- const imported = element.propertyName?.text || element.name.text
96
- if (imported === 'styled') styled.add(element.name.text)
97
- }
98
- }
99
-
100
- return { styled }
101
- }
102
-
103
- function propertyName(typescript: typeof ts, name: ts.PropertyName): string | null {
104
- if (typescript.isIdentifier(name) || typescript.isStringLiteral(name)) {
105
- return name.text
106
- }
107
- return null
108
- }
109
-
110
- function completionProperty(
111
- typescript: typeof ts,
112
- literal: ts.StringLiteralLike,
113
- tooling: StyleTooling,
114
- checker: ts.TypeChecker,
115
- bindings: ImportedBindings
116
- ): { property: string } | null {
117
- let parent = literal.parent
118
-
119
- if (typescript.isJsxExpression(parent)) parent = parent.parent
120
- if (typescript.isJsxAttribute(parent)) {
121
- if (!typescript.isIdentifier(parent.name)) return null
122
- const property = parent.name.text
123
- if (!tooling.styleProps.has(property)) return null
124
- const opening = parent.parent.parent
125
- if (!typescript.isJsxOpeningLikeElement(opening)) return null
126
- const host = resolveTamaguiHost(checker, opening.tagName)
127
- if (!host?.accepts(property)) return null
128
- return { property }
129
- }
130
-
131
- if (!typescript.isPropertyAssignment(parent)) return null
132
- const property = propertyName(typescript, parent.name)
133
- if (!property || !tooling.styleProps.has(property)) return null
134
-
135
- let current: ts.Node = parent
136
- while (current.parent && !typescript.isCallExpression(current.parent)) {
137
- current = current.parent
138
- }
139
- if (!current.parent || !typescript.isCallExpression(current.parent)) return null
140
- const call = current.parent
141
- if (
142
- call.arguments[1] !== current ||
143
- !typescript.isIdentifier(call.expression) ||
144
- !bindings.styled.has(call.expression.text)
145
- ) {
146
- return null
147
- }
148
- const component = call.arguments[0]
149
- const host = component && resolveTamaguiHost(checker, component)
150
- if (!host?.accepts(property)) return null
151
- return { property }
152
- }
153
-
154
- /**
155
- * The literal as a style value site, or null when it is not one: wrong parent
156
- * shape, not a style prop, not a tamagui host, or authored with escapes that
157
- * cook differently than they read (TypeScript exposes cooked values but no
158
- * cooked-to-source offset map, so exact-raw literals only).
159
- */
160
- function siteForLiteral(
161
- typescript: typeof ts,
162
- sourceFile: ts.SourceFile,
163
- literal: ts.StringLiteralLike,
164
- tooling: StyleTooling,
165
- checker: ts.TypeChecker,
166
- bindings: ImportedBindings
167
- ): LiteralSite | null {
168
- const site = completionProperty(typescript, literal, tooling, checker, bindings)
169
- if (!site) return null
170
- const contentStart = literal.getStart(sourceFile) + 1
171
- const contentEnd = literal.getEnd() - 1
172
- const sourceInput = sourceFile.text.slice(contentStart, contentEnd)
173
- if (literal.text !== sourceInput) return null
174
- return { property: site.property, value: literal.text, contentStart, literal }
175
- }
176
-
177
- function collectLiteralSites(
178
- typescript: typeof ts,
179
- sourceFile: ts.SourceFile,
180
- tooling: StyleTooling,
181
- checker: ts.TypeChecker,
182
- bindings: ImportedBindings
183
- ): LiteralSite[] {
184
- const sites: LiteralSite[] = []
185
- const visit = (node: ts.Node): void => {
186
- if (typescript.isStringLiteralLike(node)) {
187
- const site = siteForLiteral(
188
- typescript,
189
- sourceFile,
190
- node,
191
- tooling,
192
- checker,
193
- bindings
194
- )
195
- if (site) sites.push(site)
196
- return
197
- }
198
- typescript.forEachChild(node, visit)
199
- }
200
- visit(sourceFile)
201
- return sites
202
- }
203
-
204
- function copyLanguageService(service: ts.LanguageService): ts.LanguageService {
205
- const proxy = Object.create(null) as ts.LanguageService
206
- for (const key of Object.keys(service) as Array<keyof ts.LanguageService>) {
207
- const value = service[key]
208
- ;(proxy as unknown as Record<string, unknown>)[key] =
209
- typeof value === 'function' ? value.bind(service) : value
210
- }
211
- return proxy
212
- }
213
-
214
- const init: ts.server.PluginModuleFactory = ({ typescript }) => ({
215
- create(info) {
216
- const pluginConfig = info.config as TamaguiLanguageServicePluginConfig | undefined
217
- const configuredPath = pluginConfig?.configPath || defaultConfigPath
218
- const configPath = isAbsolute(configuredPath)
219
- ? configuredPath
220
- : resolve(info.project.getCurrentDirectory(), configuredPath)
221
- let tooling = loadTooling(info, configPath)
222
- const watcher = info.serverHost.watchFile(configPath, () => {
223
- tooling = loadTooling(info, configPath)
224
- })
225
-
226
- const proxy = copyLanguageService(info.languageService)
227
- const baseCompletions = info.languageService.getCompletionsAtPosition.bind(
228
- info.languageService
229
- )
230
- const baseDetails = info.languageService.getCompletionEntryDetails.bind(
231
- info.languageService
232
- )
233
- const baseSemanticDiagnostics = info.languageService.getSemanticDiagnostics.bind(
234
- info.languageService
235
- )
236
- const baseQuickInfo = info.languageService.getQuickInfoAtPosition.bind(
237
- info.languageService
238
- )
239
- const baseDispose = info.languageService.dispose.bind(info.languageService)
240
- const bindingsBySourceFile = new WeakMap<ts.SourceFile, ImportedBindings>()
241
-
242
- const fileContext = (
243
- fileName: string
244
- ): {
245
- sourceFile: ts.SourceFile
246
- checker: ts.TypeChecker
247
- bindings: ImportedBindings
248
- } | null => {
249
- const program = info.languageService.getProgram()
250
- const sourceFile = program?.getSourceFile(fileName)
251
- if (!program || !sourceFile) return null
252
- let bindings = bindingsBySourceFile.get(sourceFile)
253
- if (!bindings) {
254
- bindings = importedBindings(typescript, sourceFile)
255
- bindingsBySourceFile.set(sourceFile, bindings)
256
- }
257
- return { sourceFile, checker: program.getTypeChecker(), bindings }
258
- }
259
-
260
- proxy.getCompletionsAtPosition = (
261
- fileName,
262
- position,
263
- options,
264
- formattingSettings
265
- ) => {
266
- const getBaseCompletions = () =>
267
- baseCompletions(fileName, position, options, formattingSettings)
268
- if (!tooling) return getBaseCompletions()
269
- const context = fileContext(fileName)
270
- if (!context) return getBaseCompletions()
271
- const { sourceFile, checker, bindings } = context
272
- const literal = findStringLiteralAtPosition(typescript, sourceFile, position)
273
- if (!literal) return getBaseCompletions()
274
- const site = siteForLiteral(
275
- typescript,
276
- sourceFile,
277
- literal,
278
- tooling,
279
- checker,
280
- bindings
281
- )
282
- if (!site) return getBaseCompletions()
283
-
284
- const cursorCompletions = tooling.completions(
285
- site.property,
286
- site.value,
287
- position - site.contentStart
288
- )
289
- if (!cursorCompletions) return getBaseCompletions()
290
- const replacementSpan = {
291
- start: site.contentStart + cursorCompletions.replaceStart,
292
- length: cursorCompletions.replaceLength,
293
- }
294
- const entries: ts.CompletionEntry[] = []
295
- let contextualType: ts.Type | undefined
296
- for (const completion of cursorCompletions.completions) {
297
- const insertText = completion.insertText || completion.value
298
- const modifierKind =
299
- completion.kind === 'modifier'
300
- ? tooling.modifierKind(completion.value)
301
- : undefined
302
- if (completion.kind === 'keyword') {
303
- contextualType ||= checker.getContextualType(literal)
304
- if (
305
- !contextualType ||
306
- !checker.isTypeAssignableTo(
307
- checker.getStringLiteralType(insertText),
308
- contextualType
309
- )
310
- ) {
311
- continue
312
- }
313
- }
314
- entries.push({
315
- name: completion.value,
316
- kind: typescript.ScriptElementKind.string,
317
- kindModifiers: '',
318
- sortText: completionSortText(completion, modifierKind),
319
- insertText,
320
- replacementSpan,
321
- source: completionSource,
322
- labelDetails: {
323
- description: modifierKind
324
- ? `Tamagui ${modifierKind} modifier`
325
- : completion.kind === 'configured'
326
- ? 'Tamagui configured value'
327
- : 'Tamagui style keyword',
328
- },
329
- })
330
- }
331
- if (entries.length === 0) return getBaseCompletions()
332
- return {
333
- isGlobalCompletion: false,
334
- isMemberCompletion: false,
335
- isNewIdentifierLocation: true,
336
- // A completion requested immediately after whitespace has an empty
337
- // replacement span. Ask the editor to request again as the modifier
338
- // prefix grows instead of dismissing suggestions when the cursor
339
- // moves beyond that initial span.
340
- isIncomplete: true,
341
- entries,
342
- }
343
- }
344
-
345
- proxy.getCompletionEntryDetails = (
346
- fileName,
347
- position,
348
- entryName,
349
- formatOptions,
350
- source,
351
- preferences,
352
- data
353
- ) => {
354
- if (source === completionSource) {
355
- return {
356
- name: entryName,
357
- kind: typescript.ScriptElementKind.string,
358
- kindModifiers: '',
359
- displayParts: [
360
- {
361
- text: entryName,
362
- kind: 'stringLiteral',
363
- },
364
- ],
365
- documentation: [
366
- {
367
- text: 'Resolved from the active Tamagui config.',
368
- kind: 'text',
369
- },
370
- ],
371
- tags: [],
372
- }
373
- }
374
- return baseDetails(
375
- fileName,
376
- position,
377
- entryName,
378
- formatOptions,
379
- source,
380
- preferences,
381
- data
382
- )
383
- }
384
-
385
- proxy.getSemanticDiagnostics = (fileName) => {
386
- const diagnostics = [...baseSemanticDiagnostics(fileName)]
387
- if (!tooling) return diagnostics
388
- const context = fileContext(fileName)
389
- if (!context) return diagnostics
390
- const { sourceFile, checker, bindings } = context
391
- for (const site of collectLiteralSites(
392
- typescript,
393
- sourceFile,
394
- tooling,
395
- checker,
396
- bindings
397
- )) {
398
- for (const diagnostic of tooling.diagnostics(site.property, site.value)) {
399
- diagnostics.push({
400
- file: sourceFile,
401
- start: site.contentStart + diagnostic.start,
402
- length: Math.max(1, diagnostic.end - diagnostic.start),
403
- messageText: diagnostic.message,
404
- category: typescript.DiagnosticCategory.Error,
405
- code: diagnosticCode,
406
- source: completionSource,
407
- })
408
- }
409
- }
410
- return diagnostics
411
- }
412
-
413
- proxy.getQuickInfoAtPosition = (fileName, position) => {
414
- const getBaseQuickInfo = () => baseQuickInfo(fileName, position)
415
- if (!tooling) return getBaseQuickInfo()
416
- const context = fileContext(fileName)
417
- if (!context) return getBaseQuickInfo()
418
- const { sourceFile, checker, bindings } = context
419
- const literal = findStringLiteralAtPosition(typescript, sourceFile, position)
420
- if (!literal) return getBaseQuickInfo()
421
- const site = siteForLiteral(
422
- typescript,
423
- sourceFile,
424
- literal,
425
- tooling,
426
- checker,
427
- bindings
428
- )
429
- if (!site) return getBaseQuickInfo()
430
- const hover = tooling.hover(site.property, site.value, position - site.contentStart)
431
- if (!hover) return getBaseQuickInfo()
432
- return {
433
- kind: typescript.ScriptElementKind.string,
434
- kindModifiers: '',
435
- textSpan: {
436
- start: site.contentStart + hover.start,
437
- length: hover.end - hover.start,
438
- },
439
- displayParts: [{ text: hover.text, kind: 'stringLiteral' }],
440
- documentation: [{ text: hover.markdown, kind: 'text' }],
441
- }
442
- }
443
-
444
- proxy.dispose = () => {
445
- watcher.close()
446
- baseDispose()
447
- }
448
- return proxy
449
- },
450
- })
451
-
452
- export default init
1
+ export * from './core'
2
+ export * from './document'
3
+ export * from './extract-estree'
4
+ export * from './extract-sucrase'
package/types/check.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface CheckStyleFilesOptions {
6
6
  configPath?: string;
7
7
  /** explicit files to check instead of walking the root */
8
8
  files?: readonly string[];
9
+ /** validate clause payloads strictly against token and keyword vocabularies */
10
+ strict?: boolean;
9
11
  }
10
12
  export interface CheckedFile {
11
13
  /** root-relative display path */
@@ -17,6 +19,8 @@ export interface CheckStyleFilesResult {
17
19
  files: readonly CheckedFile[];
18
20
  checkedFileCount: number;
19
21
  diagnosticCount: number;
22
+ /** root-relative Tamagui projects omitted because they own another config */
23
+ skippedProjects: readonly string[];
20
24
  }
21
25
  export declare class MissingConfigArtifactError extends Error {
22
26
  constructor(configPath: string);
@@ -1,11 +1,11 @@
1
1
  {
2
- "mappings": "AAcA,cAEO,yBACA,yBACA;AAgBP,iBAAiB,uBAAuB;;CAEtC;;CAEA;;CAEA;;AAGF,iBAAiB,YAAY;;CAE3B;CACA;CACA,sBAAsB;;AAGxB,iBAAiB,sBAAsB;CACrC,gBAAgB;CAChB;CACA;;AAGF,OAAO,cAAM,mCAAmC,MAAM;CACpD,YAAY;;AAoBd,OAAO,iBAAS,uBACd,cAAc,2BACb;AAIH,OAAO,iBAAS,gBAAgB,SAAS,yBAAyB;;AAoFlE,OAAO,iBAAS,mBACd,QAAQ,uBACR,UAAS;CAAE",
2
+ "mappings": "AAeA,cAEO,yBACA,yBACA;AAuBP,iBAAiB,uBAAuB;;CAEtC;;CAEA;;CAEA;;CAEA;;AAGF,iBAAiB,YAAY;;CAE3B;CACA;CACA,sBAAsB;;AAGxB,iBAAiB,sBAAsB;CACrC,gBAAgB;CAChB;CACA;;CAEA;;AAGF,OAAO,cAAM,mCAAmC,MAAM;CACpD,YAAY;;AAgCd,OAAO,iBAAS,uBACd,cAAc,2BACb;AAIH,OAAO,iBAAS,gBAAgB,SAAS,yBAAyB;;AA+JlE,OAAO,iBAAS,mBACd,QAAQ,uBACR,UAAS;CAAE",
3
3
  "names": [],
4
4
  "sources": [
5
5
  "src/check.ts"
6
6
  ],
7
7
  "version": 3,
8
8
  "sourcesContent": [
9
- "// The project checker behind `tamagui check`.\n//\n// Node-only entry: walks a project's source files, extracts every static flat\n// value site with the sucrase tokenizer, and reports the same diagnostics the\n// editor plugin and eslint rule produce, formatted as readable code frames.\n\nimport { readdirSync, readFileSync } from 'node:fs'\nimport { join, relative } from 'node:path'\n// deep paths, spelled down to the file: sucrase publishes no `exports` map, and\n// a bare `sucrase/dist/parser` is a directory import that ESM cannot resolve\nimport { parse } from 'sucrase/dist/parser/index.js'\nimport { TokenType } from 'sucrase/dist/parser/tokenizer/types.js'\n\nimport { createStyleTooling, type SerializedConfigFile } from './core'\nimport {\n createDocumentStyleTooling,\n type DocumentDiagnostic,\n type ExtractStyleSites,\n} from './document'\nimport { createSucraseStyleSiteExtractor } from './extract-sucrase'\n\nconst sourceExtensions = /\\.(tsx|jsx)$/\nconst skippedDirectories = new Set([\n 'node_modules',\n 'dist',\n 'build',\n 'out',\n 'coverage',\n '.git',\n '.next',\n '.expo',\n '.tamagui',\n])\n\nexport interface CheckStyleFilesOptions {\n /** project root; file discovery and relative display paths anchor here */\n root: string\n /** path to the compiler's config artifact; default `<root>/.tamagui/tamagui.config.json` */\n configPath?: string\n /** explicit files to check instead of walking the root */\n files?: readonly string[]\n}\n\nexport interface CheckedFile {\n /** root-relative display path */\n file: string\n source: string\n diagnostics: readonly DocumentDiagnostic[]\n}\n\nexport interface CheckStyleFilesResult {\n files: readonly CheckedFile[]\n checkedFileCount: number\n diagnosticCount: number\n}\n\nexport class MissingConfigArtifactError extends Error {\n constructor(configPath: string) {\n super(\n `no Tamagui config artifact at ${configPath} — run your dev server or \\`tamagui generate\\` once so the compiler emits it`\n )\n this.name = 'MissingConfigArtifactError'\n }\n}\n\nfunction walkSourceFiles(directory: string, results: string[]): void {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (!skippedDirectories.has(entry.name) && !entry.name.startsWith('.')) {\n walkSourceFiles(join(directory, entry.name), results)\n }\n continue\n }\n if (sourceExtensions.test(entry.name)) results.push(join(directory, entry.name))\n }\n}\n\nexport function createProjectExtractor(\n isStyleProp: (name: string) => boolean\n): ExtractStyleSites {\n return createSucraseStyleSiteExtractor({ parse, TokenType }, { isStyleProp })\n}\n\nexport function checkStyleFiles(options: CheckStyleFilesOptions): CheckStyleFilesResult {\n const configPath =\n options.configPath ?? join(options.root, '.tamagui', 'tamagui.config.json')\n let contents: string\n try {\n contents = readFileSync(configPath, 'utf8')\n } catch {\n throw new MissingConfigArtifactError(configPath)\n }\n const tooling = createStyleTooling(JSON.parse(contents) as SerializedConfigFile)\n if (!tooling) throw new MissingConfigArtifactError(configPath)\n\n const document = createDocumentStyleTooling(\n tooling,\n createProjectExtractor((name) => tooling.isStyleProp(name))\n )\n\n let files: string[]\n if (options.files) {\n files = [...options.files]\n } else {\n files = []\n walkSourceFiles(options.root, files)\n files.sort()\n }\n\n const checked: CheckedFile[] = []\n let diagnosticCount = 0\n for (const file of files) {\n let source: string\n try {\n source = readFileSync(file, 'utf8')\n } catch {\n continue\n }\n let diagnostics: readonly DocumentDiagnostic[]\n try {\n diagnostics = document.diagnostics(source)\n } catch {\n // a file sucrase cannot parse is a syntax error the real compiler will\n // report; the style checker stays quiet about it\n continue\n }\n if (diagnostics.length === 0) continue\n diagnosticCount += diagnostics.length\n checked.push({ file: relative(options.root, file), source, diagnostics })\n }\n\n return { files: checked, checkedFileCount: files.length, diagnosticCount }\n}\n\nconst ansi = {\n red: (text: string) => `\\x1b[31m${text}\\x1b[39m`,\n dim: (text: string) => `\\x1b[2m${text}\\x1b[22m`,\n bold: (text: string) => `\\x1b[1m${text}\\x1b[22m`,\n cyan: (text: string) => `\\x1b[36m${text}\\x1b[39m`,\n}\nconst plain = {\n red: (text: string) => text,\n dim: (text: string) => text,\n bold: (text: string) => text,\n cyan: (text: string) => text,\n}\n\nfunction lineStarts(source: string): number[] {\n const starts = [0]\n for (let index = 0; index < source.length; index++) {\n if (source.charCodeAt(index) === 10) starts.push(index + 1)\n }\n return starts\n}\n\nfunction positionOf(starts: number[], offset: number): { line: number; column: number } {\n let low = 0\n let high = starts.length - 1\n while (low < high) {\n const mid = (low + high + 1) >> 1\n if (starts[mid] <= offset) low = mid\n else high = mid - 1\n }\n return { line: low, column: offset - starts[low] }\n}\n\n/** human-readable report: one code frame per diagnostic, caret-underlined */\nexport function formatCheckResults(\n result: CheckStyleFilesResult,\n options: { color?: boolean } = {}\n): string {\n const paint = options.color === false ? plain : ansi\n const output: string[] = []\n\n for (const checked of result.files) {\n const starts = lineStarts(checked.source)\n const lines = checked.source.split('\\n')\n for (const diagnostic of checked.diagnostics) {\n const start = positionOf(starts, diagnostic.start)\n const end = positionOf(starts, diagnostic.end)\n output.push(\n `${paint.bold(checked.file)}${paint.dim(`:${start.line + 1}:${start.column + 1}`)} ${paint.red('error')} ${diagnostic.message}`\n )\n const line = lines[start.line] ?? ''\n const gutter = String(start.line + 1)\n output.push(` ${paint.dim(`${gutter} │`)} ${line}`)\n const underlineLength =\n end.line === start.line\n ? Math.max(1, end.column - start.column)\n : Math.max(1, line.length - start.column)\n output.push(\n ` ${paint.dim(`${' '.repeat(gutter.length)} │`)} ${' '.repeat(start.column)}${paint.red('^'.repeat(underlineLength))}`\n )\n output.push('')\n }\n }\n\n const summary =\n result.diagnosticCount === 0\n ? `${paint.bold('✓')} ${result.checkedFileCount} files, no flat value problems`\n : `${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)`)}`\n output.push(summary)\n return output.join('\\n')\n}\n"
9
+ "// The project checker behind `tamagui check`.\n//\n// Node-only entry: walks a project's source files, extracts every static flat\n// value site with the sucrase tokenizer, and reports the same diagnostics the\n// editor plugin and eslint rule produce, formatted as readable code frames.\n\nimport { existsSync, readdirSync, readFileSync } from 'node:fs'\nimport { dirname, join, relative, resolve } from 'node:path'\nimport { spawnSync } from 'node:child_process'\n// deep paths, spelled down to the file: sucrase publishes no `exports` map, and\n// a bare `sucrase/dist/parser` is a directory import that ESM cannot resolve\nimport { parse } from 'sucrase/dist/parser/index.js'\nimport { TokenType } from 'sucrase/dist/parser/tokenizer/types.js'\n\nimport { createStyleTooling, type SerializedConfigFile } from './core'\nimport {\n createDocumentStyleTooling,\n type DocumentDiagnostic,\n type ExtractStyleSites,\n} from './document'\nimport { createSucraseStyleSiteExtractor } from './extract-sucrase'\n\nconst sourceExtensions = /\\.(tsx|jsx)$/\nconst skippedDirectories = new Set([\n 'node_modules',\n 'dist',\n 'build',\n 'out',\n 'coverage',\n '.git',\n '.next',\n '.expo',\n '.tamagui',\n])\nconst projectMarkers = [\n 'tamagui.build.ts',\n 'tamagui.build.tsx',\n 'tamagui.build.js',\n 'tamagui.build.mjs',\n 'tamagui.build.cjs',\n]\n\nexport interface CheckStyleFilesOptions {\n /** project root; file discovery and relative display paths anchor here */\n root: string\n /** path to the compiler's config artifact; default `<root>/.tamagui/tamagui.config.json` */\n configPath?: string\n /** explicit files to check instead of walking the root */\n files?: readonly string[]\n /** validate clause payloads strictly against token and keyword vocabularies */\n strict?: boolean\n}\n\nexport interface CheckedFile {\n /** root-relative display path */\n file: string\n source: string\n diagnostics: readonly DocumentDiagnostic[]\n}\n\nexport interface CheckStyleFilesResult {\n files: readonly CheckedFile[]\n checkedFileCount: number\n diagnosticCount: number\n /** root-relative Tamagui projects omitted because they own another config */\n skippedProjects: readonly string[]\n}\n\nexport class MissingConfigArtifactError extends Error {\n constructor(configPath: string) {\n super(\n `no Tamagui config artifact at ${configPath} — run your dev server or \\`tamagui generate\\` once so the compiler emits it`\n )\n this.name = 'MissingConfigArtifactError'\n }\n}\n\nfunction walkSourceFiles(\n directory: string,\n root: string,\n results: string[],\n skippedProjects: string[]\n): void {\n for (const entry of readdirSync(directory, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (skippedDirectories.has(entry.name) || entry.name.startsWith('.')) continue\n const child = join(directory, entry.name)\n if (\n projectMarkers.some((marker) => existsSync(join(child, marker))) ||\n existsSync(join(child, '.tamagui', 'tamagui.config.json'))\n ) {\n skippedProjects.push(relative(root, child))\n continue\n }\n walkSourceFiles(child, root, results, skippedProjects)\n continue\n }\n if (sourceExtensions.test(entry.name)) results.push(join(directory, entry.name))\n }\n}\n\nexport function createProjectExtractor(\n isStyleProp: (name: string) => boolean\n): ExtractStyleSites {\n return createSucraseStyleSiteExtractor({ parse, TokenType }, { isStyleProp })\n}\n\nexport function checkStyleFiles(options: CheckStyleFilesOptions): CheckStyleFilesResult {\n const root = resolve(options.root)\n const configPath = options.configPath ?? join(root, '.tamagui', 'tamagui.config.json')\n let contents: string\n try {\n contents = readFileSync(configPath, 'utf8')\n } catch {\n throw new MissingConfigArtifactError(configPath)\n }\n const tooling = createStyleTooling(JSON.parse(contents) as SerializedConfigFile, {\n strictPayloads: options.strict,\n })\n if (!tooling) throw new MissingConfigArtifactError(configPath)\n\n const document = createDocumentStyleTooling(\n tooling,\n createProjectExtractor((name) => tooling.isStyleProp(name)),\n { strictPayloads: options.strict }\n )\n\n let files: string[]\n const skippedProjects: string[] = []\n if (options.files) {\n files = [...options.files]\n } else {\n files = []\n const insideGit = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {\n cwd: root,\n encoding: 'utf8',\n })\n if (insideGit.status === 0) {\n const listed = spawnSync(\n 'git',\n [\n 'ls-files',\n '--cached',\n '--others',\n '--exclude-standard',\n '-z',\n '--',\n '*.tsx',\n '*.jsx',\n ],\n {\n cwd: root,\n encoding: 'utf8',\n maxBuffer: 64 * 1024 * 1024,\n }\n )\n if (listed.error) throw listed.error\n if (listed.status !== 0) {\n throw new Error(listed.stderr.trim() || 'git ls-files failed')\n }\n const skipped = new Set<string>()\n const projectByDirectory = new Map<string, string | null>()\n for (const relativeFile of listed.stdout.split('\\0')) {\n if (!relativeFile) continue\n const file = join(root, relativeFile)\n if (!existsSync(file)) continue\n let directory = dirname(file)\n let nestedProject: string | null = null\n const visited: string[] = []\n while (directory !== root) {\n if (projectByDirectory.has(directory)) {\n nestedProject = projectByDirectory.get(directory) ?? null\n break\n }\n visited.push(directory)\n if (\n projectMarkers.some((marker) => existsSync(join(directory, marker))) ||\n existsSync(join(directory, '.tamagui', 'tamagui.config.json'))\n ) {\n nestedProject = directory\n break\n }\n directory = dirname(directory)\n }\n for (const visitedDirectory of visited) {\n projectByDirectory.set(visitedDirectory, nestedProject)\n }\n if (nestedProject) {\n skipped.add(relative(root, nestedProject))\n } else {\n files.push(file)\n }\n }\n skippedProjects.push(...skipped)\n } else {\n walkSourceFiles(root, root, files, skippedProjects)\n }\n files.sort()\n skippedProjects.sort()\n }\n\n const checked: CheckedFile[] = []\n let diagnosticCount = 0\n for (const file of files) {\n let source: string\n try {\n source = readFileSync(file, 'utf8')\n } catch {\n continue\n }\n let diagnostics: readonly DocumentDiagnostic[]\n try {\n diagnostics = document.diagnostics(source, undefined, {\n strictPayloads: options.strict,\n })\n } catch {\n // a file sucrase cannot parse is a syntax error the real compiler will\n // report; the style checker stays quiet about it\n continue\n }\n if (diagnostics.length === 0) continue\n diagnosticCount += diagnostics.length\n checked.push({ file: relative(root, file), source, diagnostics })\n }\n\n return {\n files: checked,\n checkedFileCount: files.length,\n diagnosticCount,\n skippedProjects,\n }\n}\n\nconst ansi = {\n red: (text: string) => `\\x1b[31m${text}\\x1b[39m`,\n dim: (text: string) => `\\x1b[2m${text}\\x1b[22m`,\n bold: (text: string) => `\\x1b[1m${text}\\x1b[22m`,\n cyan: (text: string) => `\\x1b[36m${text}\\x1b[39m`,\n}\nconst plain = {\n red: (text: string) => text,\n dim: (text: string) => text,\n bold: (text: string) => text,\n cyan: (text: string) => text,\n}\n\nfunction lineStarts(source: string): number[] {\n const starts = [0]\n for (let index = 0; index < source.length; index++) {\n if (source.charCodeAt(index) === 10) starts.push(index + 1)\n }\n return starts\n}\n\nfunction positionOf(starts: number[], offset: number): { line: number; column: number } {\n let low = 0\n let high = starts.length - 1\n while (low < high) {\n const mid = (low + high + 1) >> 1\n if (starts[mid] <= offset) low = mid\n else high = mid - 1\n }\n return { line: low, column: offset - starts[low] }\n}\n\n/** human-readable report: one code frame per diagnostic, caret-underlined */\nexport function formatCheckResults(\n result: CheckStyleFilesResult,\n options: { color?: boolean } = {}\n): string {\n const paint = options.color === false ? plain : ansi\n const output: string[] = []\n\n for (const checked of result.files) {\n const starts = lineStarts(checked.source)\n const lines = checked.source.split('\\n')\n for (const diagnostic of checked.diagnostics) {\n const start = positionOf(starts, diagnostic.start)\n const end = positionOf(starts, diagnostic.end)\n output.push(\n `${paint.bold(checked.file)}${paint.dim(`:${start.line + 1}:${start.column + 1}`)} ${paint.red('error')} ${diagnostic.message}`\n )\n const line = lines[start.line] ?? ''\n const gutter = String(start.line + 1)\n output.push(` ${paint.dim(`${gutter} │`)} ${line}`)\n const underlineLength =\n end.line === start.line\n ? Math.max(1, end.column - start.column)\n : Math.max(1, line.length - start.column)\n output.push(\n ` ${paint.dim(`${' '.repeat(gutter.length)} │`)} ${' '.repeat(start.column)}${paint.red('^'.repeat(underlineLength))}`\n )\n output.push('')\n }\n }\n\n if (result.skippedProjects.length > 0) {\n output.push(\n paint.dim(`Skipped nested Tamagui projects: ${result.skippedProjects.join(', ')}`)\n )\n }\n\n const summary =\n result.diagnosticCount === 0\n ? `${paint.bold('✓')} ${result.checkedFileCount} files, no flat value problems`\n : `${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)`)}`\n output.push(summary)\n return output.join('\\n')\n}\n"
10
10
  ]
11
11
  }
package/types/core.d.ts CHANGED
@@ -51,7 +51,9 @@ export interface StyleTooling {
51
51
  */
52
52
  completions(property: string, value: string, cursor: number): StyleValueCursorCompletions | null;
53
53
  /** the complete static verdict for one authored value */
54
- diagnostics(property: string, value: string): readonly StyleValueDiagnostic[];
54
+ diagnostics(property: string, value: string, options?: {
55
+ strictPayloads?: boolean;
56
+ }): readonly StyleValueDiagnostic[];
55
57
  /** classified spans: modifiers, tokens, keywords, literals */
56
58
  annotations(property: string, value: string): readonly StyleValueAnnotation[];
57
59
  /** hover content for the annotation under `offset`, or null */
@@ -63,6 +65,9 @@ export interface StyleTooling {
63
65
  /** root theme names, preview themes first */
64
66
  previewThemes: readonly string[];
65
67
  }
66
- export declare function createStyleTooling(file: SerializedConfigFile): StyleTooling | null;
68
+ export interface StyleToolingOptions {
69
+ strictPayloads?: boolean;
70
+ }
71
+ export declare function createStyleTooling(file: SerializedConfigFile, toolingOptions?: StyleToolingOptions): StyleTooling | null;
67
72
 
68
73
  //# sourceMappingURL=core.d.ts.map