@tamagui/language-service 3.0.0-beta.924.1 → 3.0.0
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 +25 -134
- package/dist/cjs/check.cjs +85 -10
- package/dist/cjs/check.native.cjs +138 -22
- package/dist/cjs/check.native.js +138 -22
- package/dist/cjs/check.native.js.map +1 -1
- package/dist/cjs/core.cjs +8 -4
- package/dist/cjs/core.native.cjs +10 -9
- package/dist/cjs/core.native.js +10 -9
- package/dist/cjs/core.native.js.map +1 -1
- package/dist/cjs/document.cjs +4 -3
- package/dist/cjs/document.native.cjs +5 -3
- package/dist/cjs/document.native.js +5 -3
- package/dist/cjs/document.native.js.map +1 -1
- package/dist/cjs/extract-estree.native.cjs +2 -6
- package/dist/cjs/extract-estree.native.js +2 -6
- package/dist/cjs/extract-estree.native.js.map +1 -1
- package/dist/cjs/index.cjs +5 -284
- package/dist/cjs/index.native.cjs +5 -395
- package/dist/cjs/index.native.js +5 -395
- package/dist/cjs/index.native.js.map +1 -1
- package/dist/esm/check.mjs +87 -12
- package/dist/esm/check.mjs.map +1 -1
- package/dist/esm/check.native.js +140 -24
- package/dist/esm/check.native.js.map +1 -1
- package/dist/esm/core.mjs +8 -4
- package/dist/esm/core.mjs.map +1 -1
- package/dist/esm/core.native.js +10 -9
- package/dist/esm/core.native.js.map +1 -1
- package/dist/esm/document.mjs +4 -3
- package/dist/esm/document.mjs.map +1 -1
- package/dist/esm/document.native.js +5 -3
- package/dist/esm/document.native.js.map +1 -1
- package/dist/esm/extract-estree.native.js +2 -6
- package/dist/esm/extract-estree.native.js.map +1 -1
- package/dist/esm/index.mjs +5 -279
- package/dist/esm/index.native.js +5 -390
- package/package.json +8 -17
- package/src/check.ts +118 -13
- package/src/core.ts +25 -6
- package/src/document.ts +21 -4
- package/src/index.ts +4 -452
- package/types/check.d.ts +4 -0
- package/types/check.d.ts.map +2 -2
- package/types/core.d.ts +7 -2
- package/types/core.d.ts.map +2 -2
- package/types/document.d.ts +8 -2
- package/types/document.d.ts.map +2 -2
- package/types/index.d.ts +4 -7
- package/types/index.d.ts.map +2 -2
- package/dist/cjs/host.cjs +0 -37
- package/dist/cjs/host.native.cjs +0 -60
- package/dist/cjs/host.native.js +0 -62
- package/dist/cjs/host.native.js.map +0 -1
- package/dist/esm/host.mjs +0 -17
- package/dist/esm/host.mjs.map +0 -1
- package/dist/esm/host.native.js +0 -38
- package/dist/esm/host.native.js.map +0 -1
- package/dist/esm/index.mjs.map +0 -1
- package/dist/esm/index.native.js.map +0 -1
- package/index.cjs +0 -1
- package/src/host.ts +0 -32
- package/types/host.d.ts +0 -10
- 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
|
|
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(
|
|
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
|
|
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 = {
|
|
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,
|
|
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(
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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);
|
package/types/check.d.ts.map
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"mappings": "
|
|
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
|
|
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
|
|
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
|