@tamagui/language-service 0.0.0-bootstrap.0 → 3.0.0-beta.643.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -1
- package/dist/cjs/check.cjs +173 -0
- package/dist/cjs/check.native.cjs +369 -0
- package/dist/cjs/check.native.js +371 -0
- package/dist/cjs/check.native.js.map +1 -0
- package/dist/cjs/core.cjs +221 -0
- package/dist/cjs/core.native.cjs +305 -0
- package/dist/cjs/core.native.js +307 -0
- package/dist/cjs/core.native.js.map +1 -0
- package/dist/cjs/document.cjs +82 -0
- package/dist/cjs/document.native.cjs +158 -0
- package/dist/cjs/document.native.js +160 -0
- package/dist/cjs/document.native.js.map +1 -0
- package/dist/cjs/extract-estree.cjs +193 -0
- package/dist/cjs/extract-estree.native.cjs +296 -0
- package/dist/cjs/extract-estree.native.js +298 -0
- package/dist/cjs/extract-estree.native.js.map +1 -0
- package/dist/cjs/extract-sucrase.cjs +222 -0
- package/dist/cjs/extract-sucrase.native.cjs +256 -0
- package/dist/cjs/extract-sucrase.native.js +258 -0
- package/dist/cjs/extract-sucrase.native.js.map +1 -0
- package/dist/cjs/host.cjs +38 -0
- package/dist/cjs/host.native.cjs +61 -0
- package/dist/cjs/host.native.js +63 -0
- package/dist/cjs/host.native.js.map +1 -0
- package/dist/cjs/index.cjs +301 -0
- package/dist/cjs/index.native.cjs +414 -0
- package/dist/cjs/index.native.js +416 -0
- package/dist/cjs/index.native.js.map +1 -0
- package/dist/esm/check.mjs +148 -0
- package/dist/esm/check.mjs.map +1 -0
- package/dist/esm/check.native.js +342 -0
- package/dist/esm/check.native.js.map +1 -0
- package/dist/esm/core.mjs +198 -0
- package/dist/esm/core.mjs.map +1 -0
- package/dist/esm/core.native.js +280 -0
- package/dist/esm/core.native.js.map +1 -0
- package/dist/esm/document.mjs +61 -0
- package/dist/esm/document.mjs.map +1 -0
- package/dist/esm/document.native.js +135 -0
- package/dist/esm/document.native.js.map +1 -0
- package/dist/esm/extract-estree.mjs +172 -0
- package/dist/esm/extract-estree.mjs.map +1 -0
- package/dist/esm/extract-estree.native.js +273 -0
- package/dist/esm/extract-estree.native.js.map +1 -0
- package/dist/esm/extract-sucrase.mjs +201 -0
- package/dist/esm/extract-sucrase.mjs.map +1 -0
- package/dist/esm/extract-sucrase.native.js +233 -0
- package/dist/esm/extract-sucrase.native.js.map +1 -0
- package/dist/esm/host.mjs +17 -0
- package/dist/esm/host.mjs.map +1 -0
- package/dist/esm/host.native.js +38 -0
- package/dist/esm/host.native.js.map +1 -0
- package/dist/esm/index.mjs +281 -0
- package/dist/esm/index.mjs.map +1 -0
- package/dist/esm/index.native.js +392 -0
- package/dist/esm/index.native.js.map +1 -0
- package/index.cjs +1 -0
- package/package.json +79 -7
- package/src/check.ts +204 -0
- package/src/core.ts +355 -0
- package/src/document.ts +140 -0
- package/src/extract-estree.ts +251 -0
- package/src/extract-sucrase.ts +308 -0
- package/src/host.ts +32 -0
- package/src/index.ts +452 -0
- package/types/check.d.ts +31 -0
- package/types/check.d.ts.map +11 -0
- package/types/core.d.ts +68 -0
- package/types/core.d.ts.map +11 -0
- package/types/document.d.ts +42 -0
- package/types/document.d.ts.map +11 -0
- package/types/extract-estree.d.ts +19 -0
- package/types/extract-estree.d.ts.map +11 -0
- package/types/extract-sucrase.d.ts +26 -0
- package/types/extract-sucrase.d.ts.map +11 -0
- package/types/host.d.ts +10 -0
- package/types/host.d.ts.map +11 -0
- package/types/index.d.ts +9 -0
- package/types/index.d.ts.map +11 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StyleSite } from "./document";
|
|
2
|
+
export interface EstreeStyleSite extends StyleSite {
|
|
3
|
+
/** the Literal or TemplateLiteral node holding the value, for reporters */
|
|
4
|
+
node: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface EstreeExtractorOptions {
|
|
7
|
+
/** which prop names produce sites; default: every prop */
|
|
8
|
+
isStyleProp?: (name: string) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* which import sources mark components and `styled` as tamagui's.
|
|
11
|
+
* default: `tamagui`, `tamagui/*`, `@tamagui/*`
|
|
12
|
+
*/
|
|
13
|
+
isTamaguiModule?: (source: string) => boolean;
|
|
14
|
+
/** treat every capitalized JSX element as a tamagui component */
|
|
15
|
+
allComponents?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function extractStyleSitesFromEstree(program: unknown, options?: EstreeExtractorOptions): readonly EstreeStyleSite[];
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=extract-estree.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mappings": "AAWA,cAAc,iBAAiB;AAU/B,iBAAiB,wBAAwB,UAAU;;CAEjD;;AAGF,iBAAiB,uBAAuB;;CAEtC,eAAe;;;;;CAKf,mBAAmB;;CAEnB;;AA+FF,OAAO,iBAAS,4BACd,kBACA,UAAS,kCACC",
|
|
3
|
+
"names": [],
|
|
4
|
+
"sources": [
|
|
5
|
+
"src/extract-estree.ts"
|
|
6
|
+
],
|
|
7
|
+
"version": 3,
|
|
8
|
+
"sourcesContent": [
|
|
9
|
+
"// Style site extraction over an ESTree program.\n//\n// One walker serves every ESTree producer: eslint (espree/typescript-eslint),\n// oxc-parser's raw-transfer AST, and anything else emitting the standard\n// shape. Node spans read `range` when present (eslint) and `start`/`end`\n// otherwise (oxc), so the same call works in both worlds.\n//\n// The recognized site shapes match the sucrase extractor exactly:\n// string-valued JSX attributes on tamagui components, and string property\n// values anywhere inside a styled() definition object.\n\nimport type { StyleSite } from './document'\n\ninterface EstreeNode {\n type: string\n range?: readonly [number, number]\n start?: number\n end?: number\n [key: string]: unknown\n}\n\nexport interface EstreeStyleSite extends StyleSite {\n /** the Literal or TemplateLiteral node holding the value, for reporters */\n node: unknown\n}\n\nexport interface EstreeExtractorOptions {\n /** which prop names produce sites; default: every prop */\n isStyleProp?: (name: string) => boolean\n /**\n * which import sources mark components and `styled` as tamagui's.\n * default: `tamagui`, `tamagui/*`, `@tamagui/*`\n */\n isTamaguiModule?: (source: string) => boolean\n /** treat every capitalized JSX element as a tamagui component */\n allComponents?: boolean\n}\n\nfunction defaultIsTamaguiModule(source: string): boolean {\n return (\n source === 'tamagui' ||\n source.startsWith('tamagui/') ||\n source.startsWith('@tamagui/')\n )\n}\n\nfunction span(node: EstreeNode): readonly [number, number] | null {\n if (node.range) return node.range\n if (typeof node.start === 'number' && typeof node.end === 'number') {\n return [node.start, node.end]\n }\n return null\n}\n\n/** cooked string value plus its content span, or null when not static/exact */\nfunction staticString(\n node: EstreeNode | null | undefined\n): { value: string; start: number; end: number } | null {\n if (!node) return null\n const at = span(node)\n if (!at) return null\n if (node.type === 'Literal') {\n const value = node.value\n if (typeof value !== 'string') return null\n // escapes cook differently than they read; an approximate replacement\n // span could delete a runtime clause, so exact-raw literals only\n const raw = node.raw\n if (typeof raw === 'string' && raw.slice(1, -1) !== value) return null\n return { value, start: at[0] + 1, end: at[1] - 1 }\n }\n if (node.type === 'TemplateLiteral') {\n const expressions = node.expressions as EstreeNode[] | undefined\n const quasis = node.quasis as\n | Array<{ value?: { cooked?: string | null; raw?: string } }>\n | undefined\n if (expressions?.length !== 0 || quasis?.length !== 1) return null\n const cooked = quasis[0]?.value?.cooked\n if (typeof cooked !== 'string') return null\n if (quasis[0]?.value?.raw !== cooked) return null\n return { value: cooked, start: at[0] + 1, end: at[1] - 1 }\n }\n return null\n}\n\nfunction identifierName(node: EstreeNode | null | undefined): string | null {\n if (!node) return null\n if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {\n return typeof node.name === 'string' ? node.name : null\n }\n return null\n}\n\n/** the root identifier of a JSX element name (`T` in `<T.Stack.Deep>`) */\nfunction jsxRootName(name: EstreeNode | null | undefined): string | null {\n let current = name\n while (current?.type === 'JSXMemberExpression') {\n current = current.object as EstreeNode\n }\n return identifierName(current)\n}\n\nconst skipKeys = new Set([\n 'parent',\n 'loc',\n 'range',\n 'leadingComments',\n 'trailingComments',\n])\n\nfunction walk(node: EstreeNode, visit: (node: EstreeNode) => void): void {\n visit(node)\n for (const key in node) {\n if (skipKeys.has(key)) continue\n const value = node[key]\n if (Array.isArray(value)) {\n for (const item of value) {\n if (item && typeof item === 'object' && typeof item.type === 'string') {\n walk(item as EstreeNode, visit)\n }\n }\n } else if (\n value &&\n typeof value === 'object' &&\n typeof (value as EstreeNode).type === 'string'\n ) {\n walk(value as EstreeNode, visit)\n }\n }\n}\n\nexport function extractStyleSitesFromEstree(\n program: unknown,\n options: EstreeExtractorOptions = {}\n): readonly EstreeStyleSite[] {\n const isStyleProp = options.isStyleProp ?? (() => true)\n const isTamaguiModule = options.isTamaguiModule ?? defaultIsTamaguiModule\n const root = program as EstreeNode\n\n // ── pass 1: imported vocabulary ──────────────────────────────────────────\n const styledBindings = new Set<string>()\n const componentNames = new Set<string>()\n\n walk(root, (node) => {\n if (node.type === 'ImportDeclaration') {\n const source = (node.source as EstreeNode | undefined)?.value\n if (typeof source !== 'string' || !isTamaguiModule(source)) return\n for (const specifier of (node.specifiers as EstreeNode[]) || []) {\n const local = identifierName(specifier.local as EstreeNode)\n if (!local) continue\n if (specifier.type === 'ImportNamespaceSpecifier') {\n componentNames.add(local)\n continue\n }\n if (specifier.type !== 'ImportSpecifier') continue\n const importedNode = specifier.imported as EstreeNode\n const imported =\n identifierName(importedNode) ??\n (typeof importedNode?.value === 'string' ? importedNode.value : null)\n if (imported === 'styled') styledBindings.add(local)\n else if (imported && /^[A-Z]/.test(imported)) componentNames.add(local)\n }\n return\n }\n if (node.type === 'VariableDeclarator') {\n const name = identifierName(node.id as EstreeNode)\n const init = node.init as EstreeNode | undefined\n if (\n name &&\n init?.type === 'CallExpression' &&\n identifierName(init.callee as EstreeNode) !== null &&\n styledBindings.has(identifierName(init.callee as EstreeNode)!)\n ) {\n componentNames.add(name)\n }\n }\n })\n\n // ── pass 2: sites ────────────────────────────────────────────────────────\n const sites: EstreeStyleSite[] = []\n\n const pushSite = (\n property: string,\n valueNode: EstreeNode,\n kind: StyleSite['kind']\n ): void => {\n if (!isStyleProp(property)) return\n const content = staticString(valueNode)\n if (!content) return\n sites.push({\n property,\n value: content.value,\n start: content.start,\n end: content.end,\n kind,\n node: valueNode,\n })\n }\n\n const visitStyleObject = (object: EstreeNode): void => {\n for (const member of (object.properties as EstreeNode[]) || []) {\n if (member.type !== 'Property') continue\n if (member.computed) continue\n const key = member.key as EstreeNode\n const property =\n identifierName(key) ?? (typeof key?.value === 'string' ? key.value : null)\n const value = member.value as EstreeNode\n if (property && isStyleProp(property)) {\n pushSite(property, value, 'styled-property')\n continue\n }\n if (value?.type === 'ObjectExpression') {\n visitStyleObject(value)\n } else if (value?.type === 'ArrayExpression') {\n for (const element of (value.elements as Array<EstreeNode | null>) || []) {\n if (element?.type === 'ObjectExpression') visitStyleObject(element)\n }\n }\n }\n }\n\n walk(root, (node) => {\n if (node.type === 'CallExpression') {\n const callee = identifierName(node.callee as EstreeNode)\n if (!callee || !styledBindings.has(callee)) return\n const definition = (node.arguments as EstreeNode[])?.[1]\n if (definition?.type === 'ObjectExpression') visitStyleObject(definition)\n return\n }\n if (node.type === 'JSXOpeningElement') {\n const componentRoot = jsxRootName(node.name as EstreeNode)\n const accepted = options.allComponents\n ? componentRoot !== null && /^[A-Z]/.test(componentRoot)\n : componentRoot !== null && componentNames.has(componentRoot)\n if (!accepted) return\n for (const attribute of (node.attributes as EstreeNode[]) || []) {\n if (attribute.type !== 'JSXAttribute') continue\n const property = identifierName(attribute.name as EstreeNode)\n if (!property) continue\n const rawValue = attribute.value as EstreeNode | undefined\n const valueNode =\n rawValue?.type === 'JSXExpressionContainer'\n ? (rawValue.expression as EstreeNode)\n : rawValue\n if (valueNode) pushSite(property, valueNode, 'jsx-attribute')\n }\n }\n })\n\n sites.sort((a, b) => a.start - b.start)\n return sites\n}\n"
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ExtractStyleSites } from "./document";
|
|
2
|
+
export interface SucraseToken {
|
|
3
|
+
type: number;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
export interface SucraseParser {
|
|
8
|
+
parse(code: string, isJSXEnabled: boolean, isTypeScriptEnabled: boolean, isFlowEnabled: boolean): {
|
|
9
|
+
tokens: SucraseToken[];
|
|
10
|
+
};
|
|
11
|
+
TokenType: Readonly<Record<string, number>>;
|
|
12
|
+
}
|
|
13
|
+
export interface SucraseExtractorOptions {
|
|
14
|
+
/** which prop names produce sites; default: every prop */
|
|
15
|
+
isStyleProp?: (name: string) => boolean;
|
|
16
|
+
/**
|
|
17
|
+
* which import sources mark components and `styled` as tamagui's.
|
|
18
|
+
* default: `tamagui`, `tamagui/*`, `@tamagui/*`
|
|
19
|
+
*/
|
|
20
|
+
isTamaguiModule?: (source: string) => boolean;
|
|
21
|
+
/** treat every capitalized JSX element as a tamagui component */
|
|
22
|
+
allComponents?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function createSucraseStyleSiteExtractor(parser: SucraseParser, options?: SucraseExtractorOptions): ExtractStyleSites;
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=extract-sucrase.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mappings": "AAmBA,cAAc,yBAAoC;AAElD,iBAAiB,aAAa;CAC5B;CACA;CACA;;AAGF,iBAAiB,cAAc;CAC7B,MACE,cACA,uBACA,8BACA,yBACC;EAAE,QAAQ;;CACb,WAAW,SAAS;;AAGtB,iBAAiB,wBAAwB;;CAEvC,eAAe;;;;;CAKf,mBAAmB;;CAEnB;;AAWF,OAAO,iBAAS,gCACd,QAAQ,eACR,UAAS,0BACR",
|
|
3
|
+
"names": [],
|
|
4
|
+
"sources": [
|
|
5
|
+
"src/extract-sucrase.ts"
|
|
6
|
+
],
|
|
7
|
+
"version": 3,
|
|
8
|
+
"sourcesContent": [
|
|
9
|
+
"// Style site extraction over sucrase's token stream.\n//\n// The ultra-light path for in-browser IDEs: sucrase's tokenizer already\n// resolves the hard ambiguities (JSX vs TS generics, template boundaries), and\n// a token walk over its output is a few kilobytes on top. The parser is\n// injected rather than imported so a host that already bundles sucrase reuses\n// its copy:\n//\n// import { parse } from 'sucrase/dist/parser/index.js'\n// import { TokenType } from 'sucrase/dist/parser/tokenizer/types.js'\n// const extract = createSucraseStyleSiteExtractor({ parse, TokenType })\n//\n// Two site shapes are recognized, matching the eslint rule and the tsserver\n// plugin: string-valued JSX attributes on tamagui components, and string\n// property values anywhere inside a styled() call's definition object\n// (variants nest arbitrarily). Sites whose raw text differs from its cooked\n// value (escapes, entities, template substitutions) are skipped: replacing an\n// approximate span risks deleting a runtime clause.\n\nimport type { ExtractStyleSites, StyleSite } from './document'\n\nexport interface SucraseToken {\n type: number\n start: number\n end: number\n}\n\nexport interface SucraseParser {\n parse(\n code: string,\n isJSXEnabled: boolean,\n isTypeScriptEnabled: boolean,\n isFlowEnabled: boolean\n ): { tokens: SucraseToken[] }\n TokenType: Readonly<Record<string, number>>\n}\n\nexport interface SucraseExtractorOptions {\n /** which prop names produce sites; default: every prop */\n isStyleProp?: (name: string) => boolean\n /**\n * which import sources mark components and `styled` as tamagui's.\n * default: `tamagui`, `tamagui/*`, `@tamagui/*`\n */\n isTamaguiModule?: (source: string) => boolean\n /** treat every capitalized JSX element as a tamagui component */\n allComponents?: boolean\n}\n\nfunction defaultIsTamaguiModule(source: string): boolean {\n return (\n source === 'tamagui' ||\n source.startsWith('tamagui/') ||\n source.startsWith('@tamagui/')\n )\n}\n\nexport function createSucraseStyleSiteExtractor(\n parser: SucraseParser,\n options: SucraseExtractorOptions = {}\n): ExtractStyleSites {\n const { TokenType } = parser\n const T = {\n import: TokenType._import,\n string: TokenType.string,\n name: TokenType.name,\n comma: TokenType.comma,\n colon: TokenType.colon,\n eq: TokenType.eq,\n dot: TokenType.dot,\n star: TokenType.star,\n slash: TokenType.slash,\n braceL: TokenType.braceL,\n braceR: TokenType.braceR,\n dollarBraceL: TokenType.dollarBraceL,\n parenL: TokenType.parenL,\n parenR: TokenType.parenR,\n backQuote: TokenType.backQuote,\n template: TokenType.template,\n jsxTagStart: TokenType.jsxTagStart,\n jsxTagEnd: TokenType.jsxTagEnd,\n jsxName: TokenType.jsxName,\n }\n const isStyleProp = options.isStyleProp ?? (() => true)\n const isTamaguiModule = options.isTamaguiModule ?? defaultIsTamaguiModule\n\n return (source) => {\n const tokens = parser.parse(source, true, true, false).tokens\n const sites: StyleSite[] = []\n\n const text = (token: SucraseToken): string => source.slice(token.start, token.end)\n\n /** cooked content span of a string/template value token run, else null */\n const stringContent = (\n index: number\n ): { start: number; end: number; next: number } | null => {\n const token = tokens[index]\n if (token.type === T.string) {\n const content = source.slice(token.start + 1, token.end - 1)\n // escapes and JSX entities cook differently than they read; skip\n if (content.includes('\\\\') || content.includes('&')) return null\n return { start: token.start + 1, end: token.end - 1, next: index + 1 }\n }\n if (token.type === T.backQuote) {\n const inner = tokens[index + 1]\n if (inner?.type === T.backQuote) {\n return { start: token.end, end: inner.start, next: index + 2 }\n }\n if (inner?.type === T.template && tokens[index + 2]?.type === T.backQuote) {\n if (text(inner).includes('\\\\')) return null\n return { start: inner.start, end: inner.end, next: index + 3 }\n }\n }\n return null\n }\n\n // ── pass 1: imported vocabulary ────────────────────────────────────────\n const styledBindings = new Set<string>()\n const componentNames = new Set<string>()\n\n for (let index = 0; index < tokens.length; index++) {\n if (tokens[index].type !== T.import) continue\n // find the module string: the first string token before the next\n // statement-ish token; import statements are short, scan ahead\n let cursor = index + 1\n const specifiers: Array<{ imported: string; local: string }> = []\n let namespace: string | null = null\n let sawBrace = false\n while (cursor < tokens.length) {\n const token = tokens[cursor]\n if (token.type === T.string) break\n if (token.type === T.braceL) sawBrace = true\n if (token.type === T.star && tokens[cursor + 2]?.type === T.name) {\n namespace = text(tokens[cursor + 2])\n cursor += 2\n }\n if (token.type === T.name && sawBrace) {\n const imported = text(token)\n if (imported !== 'as' && imported !== 'type' && imported !== 'from') {\n let local = imported\n if (\n tokens[cursor + 1]?.type === T.name &&\n text(tokens[cursor + 1]) === 'as' &&\n tokens[cursor + 2]?.type === T.name\n ) {\n local = text(tokens[cursor + 2])\n cursor += 2\n }\n specifiers.push({ imported, local })\n }\n }\n cursor++\n }\n const moduleToken = tokens[cursor]\n if (!moduleToken || moduleToken.type !== T.string) continue\n const moduleSource = source.slice(moduleToken.start + 1, moduleToken.end - 1)\n if (!isTamaguiModule(moduleSource)) continue\n if (namespace) componentNames.add(namespace)\n for (const specifier of specifiers) {\n if (specifier.imported === 'styled') {\n styledBindings.add(specifier.local)\n } else if (/^[A-Z]/.test(specifier.imported)) {\n componentNames.add(specifier.local)\n }\n }\n index = cursor\n }\n\n // `const X = styled(...)` marks X as a tamagui component\n for (let index = 0; index + 3 < tokens.length; index++) {\n if (\n tokens[index].type === T.name &&\n tokens[index + 1].type === T.eq &&\n tokens[index + 2].type === T.name &&\n styledBindings.has(text(tokens[index + 2])) &&\n tokens[index + 3].type === T.parenL\n ) {\n componentNames.add(text(tokens[index]))\n }\n }\n\n // ── pass 2: sites ──────────────────────────────────────────────────────\n // open JSX tags nest through attribute expressions, so both trackers are\n // stacks maintained by one linear walk over brace/paren depth\n const tagStack: Array<string | null> = []\n const styledStack: Array<{ parenDepth: number; braceDepth: number; arg: number }> = []\n let parenDepth = 0\n let braceDepth = 0\n\n const jsxRootAccepted = (name: string | null): boolean => {\n if (!name) return false\n if (options.allComponents) return /^[A-Z]/.test(name)\n return componentNames.has(name)\n }\n\n for (let index = 0; index < tokens.length; index++) {\n const token = tokens[index]\n\n if (token.type === T.parenL) parenDepth++\n else if (token.type === T.parenR) {\n parenDepth--\n const styledContext = styledStack[styledStack.length - 1]\n if (styledContext && parenDepth < styledContext.parenDepth) styledStack.pop()\n } else if (token.type === T.braceL || token.type === T.dollarBraceL) {\n braceDepth++\n } else if (token.type === T.braceR) braceDepth--\n else if (token.type === T.jsxTagStart) {\n // every tag head — opening `<X`, self-closing, closing `</X` — ends at\n // exactly one jsxTagEnd, so heads push and pop unconditionally.\n // attributes only ever occur inside the innermost head; nesting comes\n // from attribute expressions like render={<Y a=\"1\" />}\n const isClosing = tokens[index + 1]?.type === T.slash\n const root =\n !isClosing && tokens[index + 1]?.type === T.jsxName\n ? text(tokens[index + 1])\n : null\n tagStack.push(root)\n continue\n } else if (token.type === T.jsxTagEnd) {\n tagStack.pop()\n continue\n }\n\n // styled(...) call opens a context\n if (\n token.type === T.name &&\n styledBindings.has(text(token)) &&\n tokens[index + 1]?.type === T.parenL\n ) {\n styledStack.push({ parenDepth: parenDepth + 1, braceDepth, arg: 0 })\n continue\n }\n\n // commas at call depth separate styled()'s arguments; only the second\n // argument is the definition object\n const styledContext = styledStack[styledStack.length - 1]\n if (\n styledContext &&\n token.type === T.comma &&\n parenDepth === styledContext.parenDepth &&\n braceDepth === styledContext.braceDepth\n ) {\n styledContext.arg++\n continue\n }\n\n // styled definition property: `{ key: 'value' }` at any nesting\n if (\n styledContext?.arg === 1 &&\n (token.type === T.name || token.type === T.string) &&\n (tokens[index - 1]?.type === T.braceL || tokens[index - 1]?.type === T.comma) &&\n tokens[index + 1]?.type === T.colon &&\n braceDepth > styledContext.braceDepth\n ) {\n const property =\n token.type === T.string\n ? source.slice(token.start + 1, token.end - 1)\n : text(token)\n const content = stringContent(index + 2)\n if (content && isStyleProp(property)) {\n const after = tokens[content.next]\n if (after?.type === T.comma || after?.type === T.braceR) {\n sites.push({\n property,\n value: source.slice(content.start, content.end),\n start: content.start,\n end: content.end,\n kind: 'styled-property',\n })\n }\n }\n continue\n }\n\n // JSX attribute: jsxName not part of the tag name itself\n if (\n token.type === T.jsxName &&\n tokens[index - 1]?.type !== T.jsxTagStart &&\n tokens[index - 1]?.type !== T.dot &&\n tokens[index - 1]?.type !== T.slash &&\n jsxRootAccepted(tagStack[tagStack.length - 1] ?? null)\n ) {\n const property = text(token)\n if (!isStyleProp(property)) continue\n if (tokens[index + 1]?.type !== T.eq) continue\n const valueIndex = index + 2\n let content: { start: number; end: number; next: number } | null = null\n if (tokens[valueIndex]?.type === T.braceL) {\n const inner = stringContent(valueIndex + 1)\n if (inner && tokens[inner.next]?.type === T.braceR) content = inner\n } else {\n content = stringContent(valueIndex)\n }\n if (content) {\n sites.push({\n property,\n value: source.slice(content.start, content.end),\n start: content.start,\n end: content.end,\n kind: 'jsx-attribute',\n })\n }\n }\n }\n\n return sites\n }\n}\n"
|
|
10
|
+
]
|
|
11
|
+
}
|
package/types/host.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HostView } from "@tamagui/style-grammar/tooling";
|
|
2
|
+
import type ts from "typescript";
|
|
3
|
+
/**
|
|
4
|
+
* resolves the style-prop surface TypeScript assigns to a Tamagui component.
|
|
5
|
+
* the staticConfig marker distinguishes Tamagui hosts from unrelated React
|
|
6
|
+
* components, while the call signatures retain styled component prop types.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveTamaguiHost(checker: ts.TypeChecker, component: ts.Node): HostView | undefined;
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=host.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mappings": "AAAA,cAAc,gBAAgB;AAC9B,YAAY,QAAQ;;;;;;AAOpB,OAAO,iBAAS,mBACd,SAAS,GAAG,aACZ,WAAW,GAAG,OACb",
|
|
3
|
+
"names": [],
|
|
4
|
+
"sources": [
|
|
5
|
+
"src/host.ts"
|
|
6
|
+
],
|
|
7
|
+
"version": 3,
|
|
8
|
+
"sourcesContent": [
|
|
9
|
+
"import type { HostView } from '@tamagui/style-grammar/tooling'\nimport type ts from 'typescript'\n\n/**\n * resolves the style-prop surface TypeScript assigns to a Tamagui component.\n * the staticConfig marker distinguishes Tamagui hosts from unrelated React\n * components, while the call signatures retain styled component prop types.\n */\nexport function resolveTamaguiHost(\n checker: ts.TypeChecker,\n component: ts.Node\n): HostView | undefined {\n const componentType = checker.getTypeAtLocation(component)\n if (!checker.getPropertyOfType(componentType, 'staticConfig')) return undefined\n\n const propsTypes: ts.Type[] = []\n for (const signature of componentType.getCallSignatures()) {\n const props = signature.getParameters()[0]\n if (props) propsTypes.push(checker.getTypeOfSymbolAtLocation(props, component))\n }\n if (propsTypes.length === 0) return undefined\n\n return {\n displayName: component.getText(),\n accepts: (property) =>\n propsTypes.some(\n (propsType) =>\n checker.getPropertyOfType(checker.getApparentType(propsType), property) !==\n undefined\n ),\n }\n}\n"
|
|
10
|
+
]
|
|
11
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type ts from "typescript";
|
|
2
|
+
export interface TamaguiLanguageServicePluginConfig {
|
|
3
|
+
/** Path to the config JSON emitted by the Tamagui compiler. */
|
|
4
|
+
configPath?: string;
|
|
5
|
+
}
|
|
6
|
+
declare const init: ts.server.PluginModuleFactory;
|
|
7
|
+
export default init;
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mappings": "AAEA,YAAY,QAAQ;AAoBpB,iBAAiB,mCAAmC;;CAElD;;AA6LF,cAAM,MAAM,GAAG,OAAO;AA8OtB,eAAe",
|
|
3
|
+
"names": [],
|
|
4
|
+
"sources": [
|
|
5
|
+
"src/index.ts"
|
|
6
|
+
],
|
|
7
|
+
"version": 3,
|
|
8
|
+
"sourcesContent": [
|
|
9
|
+
"import { isAbsolute, resolve } from 'node:path'\n\nimport type ts from 'typescript'\nimport {\n completionSortText,\n createStyleTooling,\n type SerializedConfigFile,\n type StyleTooling,\n} from './core'\nimport { resolveTamaguiHost } from './host'\n\nconst completionSource = '@tamagui/language-service'\nconst diagnosticCode = 78711\nconst defaultConfigPath = '.tamagui/tamagui.config.json'\nconst flatStyledModules: ReadonlySet<string> = new Set([\n 'tamagui',\n 'tamagui/unstyled',\n '@tamagui/core',\n '@tamagui/ui',\n '@tamagui/web',\n])\n\nexport interface TamaguiLanguageServicePluginConfig {\n /** Path to the config JSON emitted by the Tamagui compiler. */\n configPath?: string\n}\n\ntype ImportedBindings = {\n styled: ReadonlySet<string>\n}\n\ntype LiteralSite = {\n property: string\n /** cooked value, identical to its authored source text */\n value: string\n /** file offset of the value's first character */\n contentStart: number\n literal: ts.StringLiteralLike\n}\n\nfunction loadTooling(\n info: ts.server.PluginCreateInfo,\n configPath: string\n): StyleTooling | null {\n const contents = info.serverHost.readFile(configPath)\n if (contents === undefined) return null\n try {\n return createStyleTooling(JSON.parse(contents) as SerializedConfigFile)\n } catch {\n return null\n }\n}\n\nfunction findStringLiteralAtPosition(\n typescript: typeof ts,\n sourceFile: ts.SourceFile,\n position: number\n): ts.StringLiteralLike | null {\n let found: ts.StringLiteralLike | null = null\n const visit = (node: ts.Node): void => {\n if (position < node.getFullStart() || position > node.getEnd()) return\n if (\n typescript.isStringLiteralLike(node) &&\n position >= node.getStart(sourceFile) + 1 &&\n position <= node.getEnd() - 1\n ) {\n found = node\n return\n }\n typescript.forEachChild(node, visit)\n }\n visit(sourceFile)\n return found\n}\n\nfunction importedBindings(\n typescript: typeof ts,\n sourceFile: ts.SourceFile\n): ImportedBindings {\n const styled = new Set<string>()\n\n for (const statement of sourceFile.statements) {\n if (\n !typescript.isImportDeclaration(statement) ||\n !typescript.isStringLiteral(statement.moduleSpecifier)\n ) {\n continue\n }\n const source = statement.moduleSpecifier.text\n if (!flatStyledModules.has(source)) continue\n const bindings = statement.importClause?.namedBindings\n if (!bindings) continue\n if (typescript.isNamespaceImport(bindings)) continue\n for (const element of bindings.elements) {\n const imported = element.propertyName?.text || element.name.text\n if (imported === 'styled') styled.add(element.name.text)\n }\n }\n\n return { styled }\n}\n\nfunction propertyName(typescript: typeof ts, name: ts.PropertyName): string | null {\n if (typescript.isIdentifier(name) || typescript.isStringLiteral(name)) {\n return name.text\n }\n return null\n}\n\nfunction completionProperty(\n typescript: typeof ts,\n literal: ts.StringLiteralLike,\n tooling: StyleTooling,\n checker: ts.TypeChecker,\n bindings: ImportedBindings\n): { property: string } | null {\n let parent = literal.parent\n\n if (typescript.isJsxExpression(parent)) parent = parent.parent\n if (typescript.isJsxAttribute(parent)) {\n if (!typescript.isIdentifier(parent.name)) return null\n const property = parent.name.text\n if (!tooling.styleProps.has(property)) return null\n const opening = parent.parent.parent\n if (!typescript.isJsxOpeningLikeElement(opening)) return null\n const host = resolveTamaguiHost(checker, opening.tagName)\n if (!host?.accepts(property)) return null\n return { property }\n }\n\n if (!typescript.isPropertyAssignment(parent)) return null\n const property = propertyName(typescript, parent.name)\n if (!property || !tooling.styleProps.has(property)) return null\n\n let current: ts.Node = parent\n while (current.parent && !typescript.isCallExpression(current.parent)) {\n current = current.parent\n }\n if (!current.parent || !typescript.isCallExpression(current.parent)) return null\n const call = current.parent\n if (\n call.arguments[1] !== current ||\n !typescript.isIdentifier(call.expression) ||\n !bindings.styled.has(call.expression.text)\n ) {\n return null\n }\n const component = call.arguments[0]\n const host = component && resolveTamaguiHost(checker, component)\n if (!host?.accepts(property)) return null\n return { property }\n}\n\n/**\n * The literal as a style value site, or null when it is not one: wrong parent\n * shape, not a style prop, not a tamagui host, or authored with escapes that\n * cook differently than they read (TypeScript exposes cooked values but no\n * cooked-to-source offset map, so exact-raw literals only).\n */\nfunction siteForLiteral(\n typescript: typeof ts,\n sourceFile: ts.SourceFile,\n literal: ts.StringLiteralLike,\n tooling: StyleTooling,\n checker: ts.TypeChecker,\n bindings: ImportedBindings\n): LiteralSite | null {\n const site = completionProperty(typescript, literal, tooling, checker, bindings)\n if (!site) return null\n const contentStart = literal.getStart(sourceFile) + 1\n const contentEnd = literal.getEnd() - 1\n const sourceInput = sourceFile.text.slice(contentStart, contentEnd)\n if (literal.text !== sourceInput) return null\n return { property: site.property, value: literal.text, contentStart, literal }\n}\n\nfunction collectLiteralSites(\n typescript: typeof ts,\n sourceFile: ts.SourceFile,\n tooling: StyleTooling,\n checker: ts.TypeChecker,\n bindings: ImportedBindings\n): LiteralSite[] {\n const sites: LiteralSite[] = []\n const visit = (node: ts.Node): void => {\n if (typescript.isStringLiteralLike(node)) {\n const site = siteForLiteral(\n typescript,\n sourceFile,\n node,\n tooling,\n checker,\n bindings\n )\n if (site) sites.push(site)\n return\n }\n typescript.forEachChild(node, visit)\n }\n visit(sourceFile)\n return sites\n}\n\nfunction copyLanguageService(service: ts.LanguageService): ts.LanguageService {\n const proxy = Object.create(null) as ts.LanguageService\n for (const key of Object.keys(service) as Array<keyof ts.LanguageService>) {\n const value = service[key]\n ;(proxy as unknown as Record<string, unknown>)[key] =\n typeof value === 'function' ? value.bind(service) : value\n }\n return proxy\n}\n\nconst init: ts.server.PluginModuleFactory = ({ typescript }) => ({\n create(info) {\n const pluginConfig = info.config as TamaguiLanguageServicePluginConfig | undefined\n const configuredPath = pluginConfig?.configPath || defaultConfigPath\n const configPath = isAbsolute(configuredPath)\n ? configuredPath\n : resolve(info.project.getCurrentDirectory(), configuredPath)\n let tooling = loadTooling(info, configPath)\n const watcher = info.serverHost.watchFile(configPath, () => {\n tooling = loadTooling(info, configPath)\n })\n\n const proxy = copyLanguageService(info.languageService)\n const baseCompletions = info.languageService.getCompletionsAtPosition.bind(\n info.languageService\n )\n const baseDetails = info.languageService.getCompletionEntryDetails.bind(\n info.languageService\n )\n const baseSemanticDiagnostics = info.languageService.getSemanticDiagnostics.bind(\n info.languageService\n )\n const baseQuickInfo = info.languageService.getQuickInfoAtPosition.bind(\n info.languageService\n )\n const baseDispose = info.languageService.dispose.bind(info.languageService)\n const bindingsBySourceFile = new WeakMap<ts.SourceFile, ImportedBindings>()\n\n const fileContext = (\n fileName: string\n ): {\n sourceFile: ts.SourceFile\n checker: ts.TypeChecker\n bindings: ImportedBindings\n } | null => {\n const program = info.languageService.getProgram()\n const sourceFile = program?.getSourceFile(fileName)\n if (!program || !sourceFile) return null\n let bindings = bindingsBySourceFile.get(sourceFile)\n if (!bindings) {\n bindings = importedBindings(typescript, sourceFile)\n bindingsBySourceFile.set(sourceFile, bindings)\n }\n return { sourceFile, checker: program.getTypeChecker(), bindings }\n }\n\n proxy.getCompletionsAtPosition = (\n fileName,\n position,\n options,\n formattingSettings\n ) => {\n const getBaseCompletions = () =>\n baseCompletions(fileName, position, options, formattingSettings)\n if (!tooling) return getBaseCompletions()\n const context = fileContext(fileName)\n if (!context) return getBaseCompletions()\n const { sourceFile, checker, bindings } = context\n const literal = findStringLiteralAtPosition(typescript, sourceFile, position)\n if (!literal) return getBaseCompletions()\n const site = siteForLiteral(\n typescript,\n sourceFile,\n literal,\n tooling,\n checker,\n bindings\n )\n if (!site) return getBaseCompletions()\n\n const cursorCompletions = tooling.completions(\n site.property,\n site.value,\n position - site.contentStart\n )\n if (!cursorCompletions) return getBaseCompletions()\n const replacementSpan = {\n start: site.contentStart + cursorCompletions.replaceStart,\n length: cursorCompletions.replaceLength,\n }\n const entries: ts.CompletionEntry[] = []\n let contextualType: ts.Type | undefined\n for (const completion of cursorCompletions.completions) {\n const insertText = completion.insertText || completion.value\n const modifierKind =\n completion.kind === 'modifier'\n ? tooling.modifierKind(completion.value)\n : undefined\n if (completion.kind === 'keyword') {\n contextualType ||= checker.getContextualType(literal)\n if (\n !contextualType ||\n !checker.isTypeAssignableTo(\n checker.getStringLiteralType(insertText),\n contextualType\n )\n ) {\n continue\n }\n }\n entries.push({\n name: completion.value,\n kind: typescript.ScriptElementKind.string,\n kindModifiers: '',\n sortText: completionSortText(completion, modifierKind),\n insertText,\n replacementSpan,\n source: completionSource,\n labelDetails: {\n description: modifierKind\n ? `Tamagui ${modifierKind} modifier`\n : completion.kind === 'configured'\n ? 'Tamagui configured value'\n : 'Tamagui style keyword',\n },\n })\n }\n if (entries.length === 0) return getBaseCompletions()\n return {\n isGlobalCompletion: false,\n isMemberCompletion: false,\n isNewIdentifierLocation: true,\n // A completion requested immediately after whitespace has an empty\n // replacement span. Ask the editor to request again as the modifier\n // prefix grows instead of dismissing suggestions when the cursor\n // moves beyond that initial span.\n isIncomplete: true,\n entries,\n }\n }\n\n proxy.getCompletionEntryDetails = (\n fileName,\n position,\n entryName,\n formatOptions,\n source,\n preferences,\n data\n ) => {\n if (source === completionSource) {\n return {\n name: entryName,\n kind: typescript.ScriptElementKind.string,\n kindModifiers: '',\n displayParts: [\n {\n text: entryName,\n kind: 'stringLiteral',\n },\n ],\n documentation: [\n {\n text: 'Resolved from the active Tamagui config.',\n kind: 'text',\n },\n ],\n tags: [],\n }\n }\n return baseDetails(\n fileName,\n position,\n entryName,\n formatOptions,\n source,\n preferences,\n data\n )\n }\n\n proxy.getSemanticDiagnostics = (fileName) => {\n const diagnostics = [...baseSemanticDiagnostics(fileName)]\n if (!tooling) return diagnostics\n const context = fileContext(fileName)\n if (!context) return diagnostics\n const { sourceFile, checker, bindings } = context\n for (const site of collectLiteralSites(\n typescript,\n sourceFile,\n tooling,\n checker,\n bindings\n )) {\n for (const diagnostic of tooling.diagnostics(site.property, site.value)) {\n diagnostics.push({\n file: sourceFile,\n start: site.contentStart + diagnostic.start,\n length: Math.max(1, diagnostic.end - diagnostic.start),\n messageText: diagnostic.message,\n category: typescript.DiagnosticCategory.Error,\n code: diagnosticCode,\n source: completionSource,\n })\n }\n }\n return diagnostics\n }\n\n proxy.getQuickInfoAtPosition = (fileName, position) => {\n const getBaseQuickInfo = () => baseQuickInfo(fileName, position)\n if (!tooling) return getBaseQuickInfo()\n const context = fileContext(fileName)\n if (!context) return getBaseQuickInfo()\n const { sourceFile, checker, bindings } = context\n const literal = findStringLiteralAtPosition(typescript, sourceFile, position)\n if (!literal) return getBaseQuickInfo()\n const site = siteForLiteral(\n typescript,\n sourceFile,\n literal,\n tooling,\n checker,\n bindings\n )\n if (!site) return getBaseQuickInfo()\n const hover = tooling.hover(site.property, site.value, position - site.contentStart)\n if (!hover) return getBaseQuickInfo()\n return {\n kind: typescript.ScriptElementKind.string,\n kindModifiers: '',\n textSpan: {\n start: site.contentStart + hover.start,\n length: hover.end - hover.start,\n },\n displayParts: [{ text: hover.text, kind: 'stringLiteral' }],\n documentation: [{ text: hover.markdown, kind: 'text' }],\n }\n }\n\n proxy.dispose = () => {\n watcher.close()\n baseDispose()\n }\n return proxy\n },\n})\n\nexport default init\n"
|
|
10
|
+
]
|
|
11
|
+
}
|