codemap-vue 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,13 @@
1
+ """Vue SFC indexer plugin for CodeMap.
2
+
3
+ The entry-point group ``codemap.indexers`` discovers this class
4
+ automatically once ``codemap-vue`` is installed alongside the host
5
+ CodeMap CLI.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from codemap_vue.indexer import VueIndexer
11
+
12
+ __all__ = ["VueIndexer"]
13
+ __version__ = "0.2.0a1"
codemap_vue/indexer.py ADDED
@@ -0,0 +1,293 @@
1
+ """Vue SFC indexer.
2
+
3
+ Strategy
4
+ ========
5
+
6
+ ``.vue`` files mix three languages — HTML-like ``<template>``,
7
+ JS / TS in ``<script>``, and CSS / SCSS / Less in ``<style>``. Since
8
+ ``tree-sitter-vue`` is not available on PyPI, this plugin:
9
+
10
+ 1. Uses :mod:`codemap_vue.sfc` to locate every top-level ``<script>``
11
+ block in the file and read its ``lang=`` attribute.
12
+ 2. Dispatches the block's bytes to the matching tree-sitter grammar
13
+ (``tree-sitter-javascript`` for JS / JSX, ``tree-sitter-typescript``
14
+ for TS / TSX). The TypeScript grammar is an optional dependency —
15
+ ``.vue`` files that declare ``lang="ts"`` without it installed
16
+ produce a diagnostic, not a crash.
17
+ 3. Walks the inner parse tree, collecting top-level functions, classes
18
+ (with methods), and module-level ``const`` / ``let`` / ``var``
19
+ declarations. Symbol line numbers are translated from script-local
20
+ to file-global coordinates so ``codemap get`` jumps to the right
21
+ line in the ``.vue`` source.
22
+
23
+ ``<template>`` and ``<style>`` are intentionally ignored — they are
24
+ covered by neither tree-sitter-javascript nor tree-sitter-typescript,
25
+ and their template-only constructs are best served by a dedicated
26
+ ``codemap-html`` plugin if such coverage is ever needed.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from pathlib import Path, PurePosixPath
32
+ from typing import ClassVar
33
+
34
+ import tree_sitter
35
+ import tree_sitter_javascript
36
+
37
+ from codemap.core.models import Diagnostic, IndexResult, Range, Symbol
38
+ from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
39
+ from codemap.indexers.base import IndexContext
40
+ from codemap_vue.sfc import ScriptBlock, extract_script_blocks
41
+
42
+ SCHEME = "scip-vue"
43
+ LANG = "vue"
44
+
45
+ _JS_LANG = tree_sitter.Language(tree_sitter_javascript.language())
46
+
47
+ try:
48
+ import tree_sitter_typescript as _ts_module
49
+
50
+ _TS_LANG: tree_sitter.Language | None = tree_sitter.Language(_ts_module.language_typescript())
51
+ _TSX_LANG: tree_sitter.Language | None = tree_sitter.Language(_ts_module.language_tsx())
52
+ except ImportError: # pragma: no cover - exercised in integration only
53
+ _TS_LANG = None
54
+ _TSX_LANG = None
55
+
56
+
57
+ class VueIndexer:
58
+ name: ClassVar[str] = "vue"
59
+ version: ClassVar[str] = "0.2.0"
60
+ file_patterns: ClassVar[list[str]] = ["*.vue"]
61
+ languages: ClassVar[list[str]] = [LANG]
62
+
63
+ def supports(self, path: Path) -> bool:
64
+ return path.suffix == ".vue"
65
+
66
+ def index_file(
67
+ self,
68
+ path: Path,
69
+ source: bytes,
70
+ ctx: IndexContext,
71
+ ) -> IndexResult:
72
+ try:
73
+ source.decode("utf-8")
74
+ except UnicodeDecodeError as exc:
75
+ return IndexResult(
76
+ diagnostics=[
77
+ Diagnostic(
78
+ severity="error",
79
+ file=ctx.relative_path,
80
+ code="VUE002",
81
+ message=f"not valid UTF-8: {exc}",
82
+ producer=self.name,
83
+ )
84
+ ]
85
+ )
86
+
87
+ blocks = extract_script_blocks(source)
88
+ if not blocks:
89
+ return IndexResult()
90
+
91
+ symbols: list[Symbol] = []
92
+ diagnostics: list[Diagnostic] = []
93
+
94
+ for block in blocks:
95
+ lang_obj = _lang_for(block.lang)
96
+ if lang_obj is None:
97
+ diagnostics.append(
98
+ Diagnostic(
99
+ severity="warning",
100
+ file=ctx.relative_path,
101
+ range=Range(
102
+ start_line=block.content_start_line, end_line=block.content_start_line
103
+ ),
104
+ code="VUE003",
105
+ message=(
106
+ f'<script lang="{block.lang}"> requires tree-sitter-typescript; '
107
+ f"install with: pip install codemap-vue[typescript]"
108
+ ),
109
+ producer=self.name,
110
+ )
111
+ )
112
+ continue
113
+
114
+ parser = tree_sitter.Parser(lang_obj)
115
+ tree = parser.parse(block.content)
116
+ if tree.root_node.has_error:
117
+ diagnostics.append(
118
+ Diagnostic(
119
+ severity="warning",
120
+ file=ctx.relative_path,
121
+ range=Range(
122
+ start_line=block.content_start_line, end_line=block.content_start_line
123
+ ),
124
+ code="VUE001",
125
+ message="tree-sitter reported parse errors inside <script>; symbols may be incomplete",
126
+ producer=self.name,
127
+ )
128
+ )
129
+
130
+ visitor = _ScriptVisitor(ctx.relative_path, block)
131
+ visitor.visit(tree.root_node)
132
+ symbols.extend(visitor.symbols)
133
+
134
+ return IndexResult(symbols=symbols, diagnostics=diagnostics)
135
+
136
+
137
+ def _lang_for(script_lang: str) -> tree_sitter.Language | None:
138
+ if script_lang in {"js", "jsx"}:
139
+ return _JS_LANG
140
+ if script_lang == "ts":
141
+ return _TS_LANG
142
+ if script_lang == "tsx":
143
+ return _TSX_LANG
144
+ return _JS_LANG
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # AST walking (mirrors codemap-typescript / codemap-javascript)
149
+ # ---------------------------------------------------------------------------
150
+
151
+
152
+ class _ScriptVisitor:
153
+ """Walks one ``<script>`` block's AST, translating positions."""
154
+
155
+ def __init__(self, relative_path: PurePosixPath, block: ScriptBlock) -> None:
156
+ self.relative_path = relative_path
157
+ self.block = block
158
+ self.symbols: list[Symbol] = []
159
+ self._class_stack: list[str] = []
160
+
161
+ def visit(self, node: tree_sitter.Node) -> None:
162
+ kind = node.type
163
+ if kind == "function_declaration":
164
+ self._visit_function(node, is_method=False)
165
+ return
166
+ if kind == "class_declaration":
167
+ self._visit_class(node)
168
+ return
169
+ if kind == "method_definition":
170
+ self._visit_function(node, is_method=True)
171
+ return
172
+ if kind in {"lexical_declaration", "variable_declaration"}:
173
+ self._visit_top_level_declaration(node)
174
+ for child in node.children:
175
+ self.visit(child)
176
+
177
+ def _visit_class(self, node: tree_sitter.Node) -> None:
178
+ name = _name_child_text(node)
179
+ if name is None:
180
+ return
181
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.TYPE)
182
+ self.symbols.append(
183
+ Symbol(
184
+ id=sid,
185
+ kind="class",
186
+ language=LANG,
187
+ file=self.relative_path,
188
+ range=self._node_range(node),
189
+ )
190
+ )
191
+ self._class_stack.append(name)
192
+ try:
193
+ body = node.child_by_field_name("body")
194
+ if body is not None:
195
+ for child in body.children:
196
+ self.visit(child)
197
+ finally:
198
+ self._class_stack.pop()
199
+
200
+ def _visit_function(self, node: tree_sitter.Node, *, is_method: bool) -> None:
201
+ name = _name_child_text(node)
202
+ if name is None:
203
+ return
204
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.METHOD)
205
+ kind: str = "method" if is_method or self._class_stack else "function"
206
+ signature = _function_signature(node, name)
207
+ self.symbols.append(
208
+ Symbol(
209
+ id=sid,
210
+ kind=kind, # type: ignore[arg-type]
211
+ language=LANG,
212
+ file=self.relative_path,
213
+ range=self._node_range(node),
214
+ signature=signature,
215
+ extra={"vue_block_lang": self.block.lang},
216
+ )
217
+ )
218
+ body = node.child_by_field_name("body")
219
+ if body is not None:
220
+ for child in body.children:
221
+ self.visit(child)
222
+
223
+ def _visit_top_level_declaration(self, node: tree_sitter.Node) -> None:
224
+ if self._class_stack:
225
+ return
226
+ for child in node.children:
227
+ if child.type != "variable_declarator":
228
+ continue
229
+ name_node = child.child_by_field_name("name")
230
+ if name_node is None or name_node.type != "identifier":
231
+ continue
232
+ name = name_node.text.decode("utf-8") if name_node.text else ""
233
+ if not name:
234
+ continue
235
+ sid = self._make_id(name, descriptor_kind=DescriptorKind.TERM)
236
+ self.symbols.append(
237
+ Symbol(
238
+ id=sid,
239
+ kind="variable",
240
+ language=LANG,
241
+ file=self.relative_path,
242
+ range=self._node_range(child),
243
+ extra={"vue_block_lang": self.block.lang},
244
+ )
245
+ )
246
+
247
+ def _make_id(self, name: str, *, descriptor_kind: DescriptorKind) -> SymbolID:
248
+ descriptors = list(_path_namespaces(self.relative_path))
249
+ descriptors.extend(
250
+ Descriptor(name=cls, kind=DescriptorKind.TYPE) for cls in self._class_stack
251
+ )
252
+ descriptors.append(Descriptor(name=name, kind=descriptor_kind))
253
+ return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
254
+
255
+ def _node_range(self, node: tree_sitter.Node) -> Range:
256
+ """Convert script-local row to file-global row by adding block offset."""
257
+ start_row, start_col = node.start_point
258
+ end_row, end_col = node.end_point
259
+ # The block's content_start_line is 1-based and is the line where
260
+ # the content starts; node start_row is 0-based within the block.
261
+ offset = self.block.content_start_line - 1
262
+ return Range(
263
+ start_line=start_row + 1 + offset,
264
+ start_col=start_col,
265
+ end_line=max(end_row + 1 + offset, start_row + 1 + offset),
266
+ end_col=end_col,
267
+ )
268
+
269
+
270
+ # ---------------------------------------------------------------------------
271
+ # Pure helpers (shared shape with sibling indexers)
272
+ # ---------------------------------------------------------------------------
273
+
274
+
275
+ def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
276
+ return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
277
+
278
+
279
+ def _name_child_text(node: tree_sitter.Node) -> str | None:
280
+ name_node = node.child_by_field_name("name")
281
+ if name_node is None or name_node.text is None:
282
+ return None
283
+ text = name_node.text.decode("utf-8").strip()
284
+ return text or None
285
+
286
+
287
+ def _function_signature(node: tree_sitter.Node, name: str) -> str:
288
+ params = node.child_by_field_name("parameters")
289
+ params_text = ""
290
+ if params is not None and params.text is not None:
291
+ params_text = params.text.decode("utf-8")
292
+ prefix = "function" if node.type == "function_declaration" else ""
293
+ return (f"{prefix} {name}{params_text}").strip()
codemap_vue/sfc.py ADDED
@@ -0,0 +1,102 @@
1
+ """Vue SFC (Single File Component) top-level extractor.
2
+
3
+ ``tree-sitter-vue`` is not published on PyPI. The official Vue ecosystem
4
+ parses ``.vue`` files with a hand-written HTML scanner from
5
+ ``@vue/compiler-sfc``; we mirror the minimum we need with a small
6
+ regex-driven scanner that locates the top-level ``<script>`` block (and
7
+ nothing else — ``<template>`` and ``<style>`` are deliberately ignored).
8
+
9
+ The scanner is intentionally permissive about malformed input: a missing
10
+ ``</script>`` closing tag still produces a block that runs to EOF, so
11
+ indexing partial / in-progress files still yields useful symbols.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from dataclasses import dataclass
18
+ from typing import Literal
19
+
20
+ ScriptLang = Literal["ts", "tsx", "js", "jsx"]
21
+
22
+ _SCRIPT_OPEN_RE = re.compile(
23
+ rb"<script\b([^>]*)>", # captures attributes inside the open tag
24
+ re.IGNORECASE,
25
+ )
26
+ _SCRIPT_CLOSE_RE = re.compile(rb"</script\s*>", re.IGNORECASE)
27
+ _LANG_ATTR_RE = re.compile(
28
+ rb"""\blang\s*=\s*(?P<q>["'])(?P<lang>[^"']+)(?P=q)""",
29
+ re.IGNORECASE,
30
+ )
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class ScriptBlock:
35
+ """One ``<script>`` block extracted from a .vue file.
36
+
37
+ ``lang`` is normalised to one of ``"ts" | "tsx" | "js" | "jsx"``;
38
+ defaults to ``"js"`` if no ``lang=`` attribute is present.
39
+
40
+ ``content`` is the **bytes between the opening and closing tag**,
41
+ not including the tags themselves.
42
+
43
+ ``content_start_offset`` is the byte offset within the original
44
+ file where ``content`` begins; it is used by the indexer to
45
+ translate AST line numbers back to source-file coordinates.
46
+
47
+ ``content_start_line`` is the 1-based line number in the original
48
+ file where ``content`` begins.
49
+ """
50
+
51
+ lang: ScriptLang
52
+ content: bytes
53
+ content_start_offset: int
54
+ content_start_line: int
55
+
56
+
57
+ def extract_script_blocks(source: bytes) -> list[ScriptBlock]:
58
+ """Locate every top-level ``<script>`` block in a Vue SFC.
59
+
60
+ Standard Vue 3 SFCs hold at most two ``<script>`` blocks: a normal
61
+ one and a ``<script setup>`` one. We return them in the order they
62
+ appear. Inner blocks (nested ``<script>`` inside a string literal
63
+ or template) are not unlikely, but for top-level SFC parsing the
64
+ naive scan is correct in practice.
65
+ """
66
+ blocks: list[ScriptBlock] = []
67
+ cursor = 0
68
+ while cursor < len(source):
69
+ open_match = _SCRIPT_OPEN_RE.search(source, cursor)
70
+ if open_match is None:
71
+ break
72
+ attrs = open_match.group(1) or b""
73
+ content_start = open_match.end()
74
+ close_match = _SCRIPT_CLOSE_RE.search(source, content_start)
75
+ content_end = close_match.start() if close_match is not None else len(source)
76
+ content = source[content_start:content_end]
77
+ lang = _detect_lang(attrs)
78
+ blocks.append(
79
+ ScriptBlock(
80
+ lang=lang,
81
+ content=content,
82
+ content_start_offset=content_start,
83
+ content_start_line=source[:content_start].count(b"\n") + 1,
84
+ )
85
+ )
86
+ cursor = close_match.end() if close_match is not None else content_end
87
+ return blocks
88
+
89
+
90
+ def _detect_lang(attrs: bytes) -> ScriptLang:
91
+ """Detect ``lang=`` from the bytes inside ``<script ...>``."""
92
+ match = _LANG_ATTR_RE.search(attrs)
93
+ if match is None:
94
+ return "js"
95
+ raw = match.group("lang").decode("ascii", errors="replace").strip().lower()
96
+ if raw in {"ts", "typescript"}:
97
+ return "ts"
98
+ if raw == "tsx":
99
+ return "tsx"
100
+ if raw == "jsx":
101
+ return "jsx"
102
+ return "js"
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: codemap-vue
3
+ Version: 0.2.0
4
+ Summary: Vue SFC (.vue) indexer plugin for CodeMap (https://github.com/qxbyte/codemap)
5
+ Project-URL: Homepage, https://github.com/qxbyte/codemap
6
+ Author: CodeMap Contributors
7
+ License: MIT
8
+ Keywords: codemap,indexer,sfc,tree-sitter,vue
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: JavaScript
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development
13
+ Requires-Python: >=3.11
14
+ Requires-Dist: codemap-core<0.3,>=0.2.0
15
+ Requires-Dist: tree-sitter-javascript>=0.23
16
+ Requires-Dist: tree-sitter>=0.25
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8.0; extra == 'dev'
19
+ Requires-Dist: tree-sitter-typescript>=0.23; extra == 'dev'
20
+ Provides-Extra: typescript
21
+ Requires-Dist: tree-sitter-typescript>=0.23; extra == 'typescript'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # codemap-vue
25
+
26
+ > A Vue Single File Component (`.vue`) indexer for
27
+ > [CodeMap](https://github.com/qxbyte/codemap), distributed as an
28
+ > independent PyPI package.
29
+
30
+ ## What it captures
31
+
32
+ Backed by a small regex-based SFC scanner + tree-sitter:
33
+
34
+ | SFC block | Indexed | Backed by |
35
+ |---|---|---|
36
+ | `<template>` | ❌ Ignored | — |
37
+ | `<script>` (`lang="js"` / default) | ✅ | `tree-sitter-javascript` (required) |
38
+ | `<script>` (`lang="jsx"`) | ✅ | `tree-sitter-javascript` (required) |
39
+ | `<script>` (`lang="ts"`) | ✅ (with `[typescript]` extra) | `tree-sitter-typescript` (optional) |
40
+ | `<script>` (`lang="tsx"`) | ✅ (with `[typescript]` extra) | `tree-sitter-typescript` (optional) |
41
+ | `<script setup>` (`lang="ts"`) | ✅ (with `[typescript]` extra) | `tree-sitter-typescript` (optional) |
42
+ | `<style>` | ❌ Ignored | — |
43
+
44
+ Inside the `<script>` block, the same symbol kinds as `codemap-javascript`
45
+ and `codemap-typescript` are surfaced:
46
+
47
+ | AST node | Symbol kind |
48
+ |---|---|
49
+ | `function_declaration` | `function` |
50
+ | `class_declaration` | `class` |
51
+ | `method_definition` (inside class) | `method` |
52
+ | Top-level `const` / `let` / `var` | `variable` (with `extra.vue_block_lang`) |
53
+
54
+ Line numbers are translated back to the original `.vue` file coordinate
55
+ space, so `codemap get` jumps to the correct line even when the script
56
+ block starts hundreds of lines below `<template>`.
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ # JavaScript-only Vue projects (no <script lang="ts">):
62
+ pip install codemap-vue
63
+
64
+ # Vue projects that use TypeScript in <script setup lang="ts"> (the
65
+ # Vue 3 default):
66
+ pip install "codemap-vue[typescript]"
67
+ ```
68
+
69
+ `tree-sitter-typescript` is an optional dependency because some Vue
70
+ projects (Vue 2 codebases, JS-only sites) do not need it. When you index
71
+ a `.vue` file with `<script lang="ts">` and the extra is not installed,
72
+ the plugin emits a `VUE003` warning instead of crashing.
73
+
74
+ ## Implementation notes
75
+
76
+ `tree-sitter-vue` is not currently published on PyPI. Rather than
77
+ bundle a hand-compiled grammar, this plugin uses a permissive regex
78
+ scanner (`codemap_vue.sfc.extract_script_blocks`) to locate every
79
+ top-level `<script>` block in the SFC, read its `lang=` attribute, and
80
+ slice out the inner bytes. The inner bytes are then handed to the
81
+ appropriate tree-sitter grammar.
82
+
83
+ The scanner is intentionally permissive about malformed input: a
84
+ missing `</script>` closing tag still produces a block that runs to
85
+ EOF, so indexing partial / in-progress files still yields useful
86
+ symbols.
87
+
88
+ ## SymbolID encoding
89
+
90
+ ```
91
+ scip-vue . . . src/components/UserList.vue/fetchUser().
92
+ └──────┘ └────────────────────────────────────────┘
93
+ scheme file → namespaces / type / method
94
+ ```
95
+
96
+ ## Tests
97
+
98
+ ```bash
99
+ pip install -e ".[dev]"
100
+ pytest
101
+ ```
102
+
103
+ ## Limits / next steps
104
+
105
+ * `<template>` contents (directives, expressions, `v-on` handlers) are
106
+ not parsed. A future `codemap-vue` minor release could expose
107
+ declared template refs (`<input ref="emailRef" />`) and `defineProps`
108
+ / `defineEmits` macros as symbols.
109
+ * `<script setup>` macros (`defineProps`, `defineExpose`, …) are
110
+ surfaced only when they appear as ordinary function calls inside a
111
+ variable declaration. Treating them as first-class component-shape
112
+ symbols is a v0.2.x improvement.
113
+ * Class-based component decorators (vue-class-component / vue-property-decorator)
114
+ are not yet captured.
115
+
116
+ ## License
117
+
118
+ MIT — same as the host project.
@@ -0,0 +1,7 @@
1
+ codemap_vue/__init__.py,sha256=QRbuiJVrKuBBoA6EmPRpPsJxuUytfZ-vMWxYFIgY1Bs,318
2
+ codemap_vue/indexer.py,sha256=kxAMF3XWrDY1-DU-_V09HC0XxVka9SvM7s-k9IkpRRs,10900
3
+ codemap_vue/sfc.py,sha256=2u6ztLAmNWaQJ_o1nim9RqXu2QnWSaXSIEUxkWDNUHU,3600
4
+ codemap_vue-0.2.0.dist-info/METADATA,sha256=Jf8B3EtnQn5OQhoH0H--OKb-LO2alYfbzM--Me2QBuQ,4365
5
+ codemap_vue-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
6
+ codemap_vue-0.2.0.dist-info/entry_points.txt,sha256=mIANMB0J94jMnTkyvZfiBVTcmXcKWH9NYtF8yKQvgmQ,48
7
+ codemap_vue-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [codemap.indexers]
2
+ vue = codemap_vue:VueIndexer