@tamagui/language-service 3.0.0-beta.1216.1 → 3.0.0-beta.1309.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,140 +1,31 @@
1
1
  # `@tamagui/language-service`
2
2
 
3
- TypeScript language-service completions for Tamagui flat style values. The
4
- plugin reads the `.tamagui/tamagui.config.json` file emitted from the active
5
- Tamagui runtime config, then delegates its vocabulary, parsing, and
6
- property-family validation to `@tamagui/style-grammar`.
7
-
8
- ```json
9
- {
10
- "compilerOptions": {
11
- "plugins": [{ "name": "@tamagui/language-service" }]
12
- }
13
- }
14
- ```
15
-
16
- VS Code must also be able to resolve the plugin package. Select the workspace
17
- TypeScript version, or add this machine setting and restart the TypeScript
18
- server:
19
-
20
- ```json
21
- {
22
- "js/ts.tsserver.pluginPaths": ["node_modules"]
23
- }
3
+ Shared flat-value tooling for editors, ESLint, and `tamagui check`.
4
+
5
+ The package reads the `.tamagui/tamagui.config.json` artifact emitted by the
6
+ compiler. Its entry points project the same grammar into completion,
7
+ diagnostic, hover, color, and project-checking APIs:
8
+
9
+ | entry | purpose |
10
+ | --- | --- |
11
+ | `@tamagui/language-service` | browser-safe core, document, and extractor APIs |
12
+ | `@tamagui/language-service/core` | tooling for one property and value |
13
+ | `@tamagui/language-service/document` | tooling mapped to source offsets |
14
+ | `@tamagui/language-service/extract-sucrase` | token-stream JSX extraction |
15
+ | `@tamagui/language-service/extract-estree` | ESTree JSX extraction |
16
+ | `@tamagui/language-service/check` | Node project checker used by `tamagui check` |
17
+
18
+ Editor integration is owned by the standalone Rust language server in
19
+ `@tamagui/lsp`. It speaks standard LSP and does not load TypeScript or install a
20
+ tsserver plugin. The VS Code extension is a thin client for the same binary used
21
+ by other editors.
22
+
23
+ ```bash
24
+ npm install --save-dev @tamagui/lsp
24
25
  ```
25
26
 
26
- `js/ts.tsserver.pluginPaths` is machine-scoped, so VS Code ignores it in a
27
- workspace `.vscode/settings.json` file.
28
-
29
- VS Code also disables automatic suggestions inside strings by default. Enable
30
- them for TSX if completion should open after typing a space in a value:
27
+ For CI and pre-commit checks:
31
28
 
32
- ```json
33
- {
34
- "[typescriptreact]": {
35
- "editor.quickSuggestions": {
36
- "strings": "on"
37
- }
38
- }
39
- }
29
+ ```bash
30
+ tamagui check
40
31
  ```
41
-
42
- VS Code's TypeScript provider does not register `:` as a completion trigger.
43
- The companion Tamagui Language Service extension retriggers suggestions after a
44
- modifier colon and delegates the resulting request back to this plugin.
45
-
46
- If the generated config lives elsewhere, set `configPath` relative to the
47
- TypeScript project:
48
-
49
- ```json
50
- {
51
- "compilerOptions": {
52
- "plugins": [
53
- {
54
- "name": "@tamagui/language-service",
55
- "configPath": ".tamagui/tamagui.config.json"
56
- }
57
- ]
58
- }
59
- }
60
- ```
61
-
62
- The plugin completes configured values and modifiers in base values and
63
- conditional clause payloads on components carrying Tamagui's `staticConfig`
64
- type marker and configurations passed to the core Tamagui `styled()` export.
65
- TypeScript's contextual prop type remains the authority for whether the host
66
- accepts that property and value.
67
-
68
- Modifier suggestions are sorted as state, group, media, container, root theme,
69
- then platform. `hover:` and `press:` lead the state group. Nested themes remain
70
- valid `<Theme>` targets but are not flat-value modifiers, so an active
71
- `dark_blue` theme matches `dark:` and never adds a `dark_blue:` suggestion.
72
- After a modifier colon, the plugin offers both property values and another
73
- modifier, so `margin="4 sm:h"` completes to `margin="4 sm:hover:"`.
74
- State continuations such as `hover:` and `press:` sort first, followed by the
75
- other modifier categories and configured property values. A finished chain such
76
- as `margin="4 web:sm:hover:"` still includes its payload tokens in that list.
77
- Modifier completion traverses a trie derived from the generated config. The
78
- canonical authoring order is platform, root theme, container, viewport media,
79
- group state, then subject state. A trie node omits conditions already used,
80
- aliases of used conditions, additional platforms or root themes, earlier
81
- categories, and conditions beyond the emitter's five non-platform limit.
82
- Each category occupies one slot in a clause. For example, `hover:` offers
83
- payload values and never another subject state such as `press:`, while `sm:`
84
- never offers another viewport-media modifier.
85
- Runtime parsing remains order-insensitive for compatibility, but tooling only
86
- authors the canonical shape. A completed payload resets traversal, so every new
87
- space-delimited clause starts from the trie root at arbitrary program length.
88
-
89
- Container conditions include their size: `@sm:` targets the nearest container
90
- and `@sm/layout:` targets a named container. `@container:` is not a modifier.
91
-
92
- It does not carry a fallback vocabulary. A missing or invalid generated config
93
- produces no Tamagui completions, so the editor cannot suggest names the runtime
94
- config does not define. The generated config is watched and its vocabulary is
95
- reloaded without restarting tsserver.
96
-
97
- ## Package layout: one engine, many hosts
98
-
99
- `@tamagui/style-grammar` owns what every value MEANS. This package projects
100
- that engine into editor- and checker-shaped results, and each host stays a
101
- thin adapter:
102
-
103
- | entry | runs in | provides |
104
- | --- | --- | --- |
105
- | `@tamagui/language-service` | tsserver | completions, diagnostics, hover (quick info) |
106
- | `@tamagui/language-service/core` | anywhere (browser-safe) | `createStyleTooling(configJson)`: completions, diagnostics, annotations, hover, color swatches for one prop/value |
107
- | `@tamagui/language-service/document` | anywhere (browser-safe) | `createDocumentStyleTooling(tooling, extract)`: the same, mapped to file offsets |
108
- | `@tamagui/language-service/extract-sucrase` | anywhere (browser-safe) | token-stream site extraction; inject your own sucrase `parse`/`TokenType` so bundles never duplicate it |
109
- | `@tamagui/language-service/extract-estree` | anywhere | the same site contract from any ESTree program (eslint, oxc-parser) |
110
- | `@tamagui/language-service/check` | node | `checkStyleFiles` + `formatCheckResults`, the engine behind `tamagui check` |
111
-
112
- Every path takes the same input: the `.tamagui/tamagui.config.json` artifact
113
- the compiler emits. `core`, `document`, and `extract-sucrase` have no node,
114
- typescript, or sucrase imports — bundled together they are ~15KB gzipped — so
115
- an in-browser IDE embeds them directly:
116
-
117
- ```ts
118
- import { createStyleTooling } from '@tamagui/language-service/core'
119
- import { createDocumentStyleTooling } from '@tamagui/language-service/document'
120
- import { createSucraseStyleSiteExtractor } from '@tamagui/language-service/extract-sucrase'
121
- import { parse } from 'sucrase/dist/parser/index.js'
122
- import { TokenType } from 'sucrase/dist/parser/tokenizer/types.js'
123
-
124
- const tooling = createStyleTooling(configJson)
125
- const document = createDocumentStyleTooling(
126
- tooling,
127
- createSucraseStyleSiteExtractor({ parse, TokenType }, {
128
- isStyleProp: (name) => tooling.isStyleProp(name),
129
- })
130
- )
131
- document.completionsAt(source, offset)
132
- document.diagnostics(source)
133
- document.hoverAt(source, offset)
134
- document.colors(source)
135
- ```
136
-
137
- The VS Code extension (`code/core/language-service-vscode`) contributes this
138
- plugin through `typescriptServerPlugins` (zero per-project tsconfig setup),
139
- adds inline color swatches through the document API, and retriggers
140
- completions after modifier colons.
@@ -2,12 +2,6 @@ var __defProp = Object.defineProperty;
2
2
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
4
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __export = (target, all) => {
6
- for (var name in all) __defProp(target, name, {
7
- get: all[name],
8
- enumerable: true
9
- });
10
- };
11
5
  var __copyProps = (to, from, except, desc) => {
12
6
  if (from && typeof from === "object" || typeof from === "function") {
13
7
  for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
@@ -17,284 +11,11 @@ var __copyProps = (to, from, except, desc) => {
17
11
  }
18
12
  return to;
19
13
  };
14
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
20
15
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
16
  var index_exports = {};
22
- __export(index_exports, { default: () => index_default });
23
17
  module.exports = __toCommonJS(index_exports);
24
- var import_node_path = require("node:path");
25
- var import_core = require("./core.cjs");
26
- var import_host = require("./host.cjs");
27
- const completionSource = "@tamagui/language-service";
28
- const diagnosticCode = 78711;
29
- const defaultConfigPath = ".tamagui/tamagui.config.json";
30
- const flatStyledModules = /* @__PURE__ */ new Set([
31
- "tamagui",
32
- "tamagui/unstyled",
33
- "@tamagui/core",
34
- "@tamagui/ui",
35
- "@tamagui/web"
36
- ]);
37
- function loadTooling(info, configPath) {
38
- const contents = info.serverHost.readFile(configPath);
39
- if (contents === void 0) return null;
40
- try {
41
- return (0, import_core.createStyleTooling)(JSON.parse(contents));
42
- } catch {
43
- return null;
44
- }
45
- }
46
- function findStringLiteralAtPosition(typescript, sourceFile, position) {
47
- let found = null;
48
- const visit = (node) => {
49
- if (position < node.getFullStart() || position > node.getEnd()) return;
50
- if (typescript.isStringLiteralLike(node) && position >= node.getStart(sourceFile) + 1 && position <= node.getEnd() - 1) {
51
- found = node;
52
- return;
53
- }
54
- typescript.forEachChild(node, visit);
55
- };
56
- visit(sourceFile);
57
- return found;
58
- }
59
- function importedBindings(typescript, sourceFile) {
60
- const styled = /* @__PURE__ */ new Set();
61
- for (const statement of sourceFile.statements) {
62
- if (!typescript.isImportDeclaration(statement) || !typescript.isStringLiteral(statement.moduleSpecifier)) {
63
- continue;
64
- }
65
- const source = statement.moduleSpecifier.text;
66
- if (!flatStyledModules.has(source)) continue;
67
- const bindings = statement.importClause?.namedBindings;
68
- if (!bindings) continue;
69
- if (typescript.isNamespaceImport(bindings)) continue;
70
- for (const element of bindings.elements) {
71
- const imported = element.propertyName?.text || element.name.text;
72
- if (imported === "styled") styled.add(element.name.text);
73
- }
74
- }
75
- return { styled };
76
- }
77
- function propertyName(typescript, name) {
78
- if (typescript.isIdentifier(name) || typescript.isStringLiteral(name)) {
79
- return name.text;
80
- }
81
- return null;
82
- }
83
- function completionProperty(typescript, literal, tooling, checker, bindings) {
84
- let parent = literal.parent;
85
- if (typescript.isJsxExpression(parent)) parent = parent.parent;
86
- if (typescript.isJsxAttribute(parent)) {
87
- if (!typescript.isIdentifier(parent.name)) return null;
88
- const property2 = parent.name.text;
89
- if (!tooling.styleProps.has(property2)) return null;
90
- const opening = parent.parent.parent;
91
- if (!typescript.isJsxOpeningLikeElement(opening)) return null;
92
- const host2 = (0, import_host.resolveTamaguiHost)(checker, opening.tagName);
93
- if (!host2?.accepts(property2)) return null;
94
- return { property: property2 };
95
- }
96
- if (!typescript.isPropertyAssignment(parent)) return null;
97
- const property = propertyName(typescript, parent.name);
98
- if (!property || !tooling.styleProps.has(property)) return null;
99
- let current = parent;
100
- while (current.parent && !typescript.isCallExpression(current.parent)) {
101
- current = current.parent;
102
- }
103
- if (!current.parent || !typescript.isCallExpression(current.parent)) return null;
104
- const call = current.parent;
105
- if (call.arguments[1] !== current || !typescript.isIdentifier(call.expression) || !bindings.styled.has(call.expression.text)) {
106
- return null;
107
- }
108
- const component = call.arguments[0];
109
- const host = component && (0, import_host.resolveTamaguiHost)(checker, component);
110
- if (!host?.accepts(property)) return null;
111
- return { property };
112
- }
113
- function siteForLiteral(typescript, sourceFile, literal, tooling, checker, bindings) {
114
- const site = completionProperty(typescript, literal, tooling, checker, bindings);
115
- if (!site) return null;
116
- const contentStart = literal.getStart(sourceFile) + 1;
117
- const contentEnd = literal.getEnd() - 1;
118
- const sourceInput = sourceFile.text.slice(contentStart, contentEnd);
119
- if (literal.text !== sourceInput) return null;
120
- return {
121
- property: site.property,
122
- value: literal.text,
123
- contentStart,
124
- literal
125
- };
126
- }
127
- function collectLiteralSites(typescript, sourceFile, tooling, checker, bindings) {
128
- const sites = [];
129
- const visit = (node) => {
130
- if (typescript.isStringLiteralLike(node)) {
131
- const site = siteForLiteral(typescript, sourceFile, node, tooling, checker, bindings);
132
- if (site) sites.push(site);
133
- return;
134
- }
135
- typescript.forEachChild(node, visit);
136
- };
137
- visit(sourceFile);
138
- return sites;
139
- }
140
- function copyLanguageService(service) {
141
- const proxy = /* @__PURE__ */ Object.create(null);
142
- for (const key of Object.keys(service)) {
143
- const value = service[key];
144
- proxy[key] = typeof value === "function" ? value.bind(service) : value;
145
- }
146
- return proxy;
147
- }
148
- const init = ({ typescript }) => ({ create(info) {
149
- const pluginConfig = info.config;
150
- const configuredPath = pluginConfig?.configPath || defaultConfigPath;
151
- const configPath = (0, import_node_path.isAbsolute)(configuredPath) ? configuredPath : (0, import_node_path.resolve)(info.project.getCurrentDirectory(), configuredPath);
152
- let tooling = loadTooling(info, configPath);
153
- const watcher = info.serverHost.watchFile(configPath, () => {
154
- tooling = loadTooling(info, configPath);
155
- });
156
- const proxy = copyLanguageService(info.languageService);
157
- const baseCompletions = info.languageService.getCompletionsAtPosition.bind(info.languageService);
158
- const baseDetails = info.languageService.getCompletionEntryDetails.bind(info.languageService);
159
- const baseSemanticDiagnostics = info.languageService.getSemanticDiagnostics.bind(info.languageService);
160
- const baseQuickInfo = info.languageService.getQuickInfoAtPosition.bind(info.languageService);
161
- const baseDispose = info.languageService.dispose.bind(info.languageService);
162
- const bindingsBySourceFile = /* @__PURE__ */ new WeakMap();
163
- const fileContext = (fileName) => {
164
- const program = info.languageService.getProgram();
165
- const sourceFile = program?.getSourceFile(fileName);
166
- if (!program || !sourceFile) return null;
167
- let bindings = bindingsBySourceFile.get(sourceFile);
168
- if (!bindings) {
169
- bindings = importedBindings(typescript, sourceFile);
170
- bindingsBySourceFile.set(sourceFile, bindings);
171
- }
172
- return {
173
- sourceFile,
174
- checker: program.getTypeChecker(),
175
- bindings
176
- };
177
- };
178
- proxy.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
179
- const getBaseCompletions = () => baseCompletions(fileName, position, options, formattingSettings);
180
- if (!tooling) return getBaseCompletions();
181
- const context = fileContext(fileName);
182
- if (!context) return getBaseCompletions();
183
- const { sourceFile, checker, bindings } = context;
184
- const literal = findStringLiteralAtPosition(typescript, sourceFile, position);
185
- if (!literal) return getBaseCompletions();
186
- const site = siteForLiteral(typescript, sourceFile, literal, tooling, checker, bindings);
187
- if (!site) return getBaseCompletions();
188
- const cursorCompletions = tooling.completions(site.property, site.value, position - site.contentStart);
189
- if (!cursorCompletions) return getBaseCompletions();
190
- const replacementSpan = {
191
- start: site.contentStart + cursorCompletions.replaceStart,
192
- length: cursorCompletions.replaceLength
193
- };
194
- const entries = [];
195
- let contextualType;
196
- for (const completion of cursorCompletions.completions) {
197
- const insertText = completion.insertText || completion.value;
198
- const modifierKind = completion.kind === "modifier" ? tooling.modifierKind(completion.value) : void 0;
199
- if (completion.kind === "keyword") {
200
- contextualType ||= checker.getContextualType(literal);
201
- if (!contextualType || !checker.isTypeAssignableTo(checker.getStringLiteralType(insertText), contextualType)) {
202
- continue;
203
- }
204
- }
205
- entries.push({
206
- name: completion.value,
207
- kind: typescript.ScriptElementKind.string,
208
- kindModifiers: "",
209
- sortText: (0, import_core.completionSortText)(completion, modifierKind),
210
- insertText,
211
- replacementSpan,
212
- source: completionSource,
213
- labelDetails: { description: modifierKind ? `Tamagui ${modifierKind} modifier` : completion.kind === "configured" ? "Tamagui configured value" : "Tamagui style keyword" }
214
- });
215
- }
216
- if (entries.length === 0) return getBaseCompletions();
217
- return {
218
- isGlobalCompletion: false,
219
- isMemberCompletion: false,
220
- isNewIdentifierLocation: true,
221
- isIncomplete: true,
222
- entries
223
- };
224
- };
225
- proxy.getCompletionEntryDetails = (fileName, position, entryName, formatOptions, source, preferences, data) => {
226
- if (source === completionSource) {
227
- return {
228
- name: entryName,
229
- kind: typescript.ScriptElementKind.string,
230
- kindModifiers: "",
231
- displayParts: [{
232
- text: entryName,
233
- kind: "stringLiteral"
234
- }],
235
- documentation: [{
236
- text: "Resolved from the active Tamagui config.",
237
- kind: "text"
238
- }],
239
- tags: []
240
- };
241
- }
242
- return baseDetails(fileName, position, entryName, formatOptions, source, preferences, data);
243
- };
244
- proxy.getSemanticDiagnostics = (fileName) => {
245
- const diagnostics = [...baseSemanticDiagnostics(fileName)];
246
- if (!tooling) return diagnostics;
247
- const context = fileContext(fileName);
248
- if (!context) return diagnostics;
249
- const { sourceFile, checker, bindings } = context;
250
- for (const site of collectLiteralSites(typescript, sourceFile, tooling, checker, bindings)) {
251
- for (const diagnostic of tooling.diagnostics(site.property, site.value)) {
252
- diagnostics.push({
253
- file: sourceFile,
254
- start: site.contentStart + diagnostic.start,
255
- length: Math.max(1, diagnostic.end - diagnostic.start),
256
- messageText: diagnostic.message,
257
- category: typescript.DiagnosticCategory.Error,
258
- code: diagnosticCode,
259
- source: completionSource
260
- });
261
- }
262
- }
263
- return diagnostics;
264
- };
265
- proxy.getQuickInfoAtPosition = (fileName, position) => {
266
- const getBaseQuickInfo = () => baseQuickInfo(fileName, position);
267
- if (!tooling) return getBaseQuickInfo();
268
- const context = fileContext(fileName);
269
- if (!context) return getBaseQuickInfo();
270
- const { sourceFile, checker, bindings } = context;
271
- const literal = findStringLiteralAtPosition(typescript, sourceFile, position);
272
- if (!literal) return getBaseQuickInfo();
273
- const site = siteForLiteral(typescript, sourceFile, literal, tooling, checker, bindings);
274
- if (!site) return getBaseQuickInfo();
275
- const hover = tooling.hover(site.property, site.value, position - site.contentStart);
276
- if (!hover) return getBaseQuickInfo();
277
- return {
278
- kind: typescript.ScriptElementKind.string,
279
- kindModifiers: "",
280
- textSpan: {
281
- start: site.contentStart + hover.start,
282
- length: hover.end - hover.start
283
- },
284
- displayParts: [{
285
- text: hover.text,
286
- kind: "stringLiteral"
287
- }],
288
- documentation: [{
289
- text: hover.markdown,
290
- kind: "text"
291
- }]
292
- };
293
- };
294
- proxy.dispose = () => {
295
- watcher.close();
296
- baseDispose();
297
- };
298
- return proxy;
299
- } });
300
- var index_default = init;
18
+ __reExport(index_exports, require("./core.cjs"), module.exports);
19
+ __reExport(index_exports, require("./document.cjs"), module.exports);
20
+ __reExport(index_exports, require("./extract-estree.cjs"), module.exports);
21
+ __reExport(index_exports, require("./extract-sucrase.cjs"), module.exports);