blasttrace 1.2.0__py2.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.

Potentially problematic release.


This version of blasttrace might be problematic. Click here for more details.

@@ -0,0 +1,299 @@
1
+ """
2
+ BlastTrace TypeScript & React Frontend Parser
3
+ Parses TypeScript/JavaScript functions, React components, interfaces/types, ES imports,
4
+ API consumer calls (fetch, axios, useSWR, useQuery), and response property accesses.
5
+ """
6
+
7
+ import os
8
+ import re
9
+ from typing import Dict, Any, List
10
+
11
+
12
+ def _normalize_url_to_route_path(raw_url: str) -> str:
13
+ cleaned = raw_url.strip().strip("\"'`")
14
+ cleaned = re.sub(r"^https?://[^/]+", "", cleaned)
15
+ cleaned = cleaned.split("?")[0]
16
+ cleaned = re.sub(r"\$\{[^}]+\}", ":param", cleaned)
17
+ cleaned = re.sub(r"\{[^}]+\}", ":param", cleaned)
18
+ if not cleaned.startswith("/"):
19
+ cleaned = "/" + cleaned
20
+ if len(cleaned) > 1 and cleaned.endswith("/"):
21
+ cleaned = cleaned.rstrip("/")
22
+ return cleaned
23
+
24
+
25
+ def _parse_ts_params(param_str: str) -> List[Dict[str, Any]]:
26
+ params: List[Dict[str, Any]] = []
27
+ raw = param_str.strip()
28
+ if not raw:
29
+ return params
30
+ # Handle destructured props like `{ userId, amount = 0 }: CheckoutProps`
31
+ if raw.startswith("{"):
32
+ destruct_match = re.match(r"\{([^}]*)\}(?:\s*:\s*([A-Za-z0-9_<>[\]|&]+))?", raw)
33
+ if destruct_match:
34
+ inner = destruct_match.group(1)
35
+ prop_type = (destruct_match.group(2) or "").strip()
36
+ for part in inner.split(","):
37
+ p = part.strip()
38
+ if not p:
39
+ continue
40
+ has_def = "=" in p
41
+ p_name = p.split("=")[0].split(":")[0].strip()
42
+ def_val = p.split("=", 1)[1].strip() if has_def else None
43
+ params.append({
44
+ "name": p_name,
45
+ "annotation": prop_type,
46
+ "required": not has_def,
47
+ "default": def_val,
48
+ })
49
+ return params
50
+
51
+ for part in raw.split(","):
52
+ p = part.strip()
53
+ if not p:
54
+ continue
55
+ has_def = "=" in p
56
+ left = p.split("=", 1)[0].strip()
57
+ def_val = p.split("=", 1)[1].strip() if has_def else None
58
+ optional_q = "?" in left.split(":")[0]
59
+ if ":" in left:
60
+ p_name, p_ann = left.split(":", 1)
61
+ p_name = p_name.strip().rstrip("?")
62
+ p_ann = p_ann.strip()
63
+ else:
64
+ p_name = left.rstrip("?")
65
+ p_ann = ""
66
+ params.append({
67
+ "name": p_name,
68
+ "annotation": p_ann,
69
+ "required": not (has_def or optional_q),
70
+ "default": def_val,
71
+ })
72
+ return params
73
+
74
+
75
+ def parse_ts_react_content(content: str, rel_file: str) -> Dict[str, Any]:
76
+ symbols: Dict[str, Dict[str, Any]] = {}
77
+ interfaces: Dict[str, Dict[str, Any]] = {}
78
+ imports: List[Dict[str, str]] = []
79
+ api_calls: List[Dict[str, Any]] = []
80
+ property_accesses: List[str] = []
81
+ nodes: List[Dict[str, Any]] = []
82
+ edges: List[Dict[str, str]] = []
83
+
84
+ module_id = f"fe:{rel_file}"
85
+ nodes.append({
86
+ "id": module_id,
87
+ "type": "frontend_module",
88
+ "name": rel_file,
89
+ "file": rel_file,
90
+ "layer": "frontend",
91
+ })
92
+
93
+ # 1. ES Imports
94
+ import_pattern = re.compile(
95
+ r"import\s+(?:type\s+)?(?:\{([^}]+)\}|([A-Za-z0-9_$]+))\s+from\s+[`\"']([^`\"']+)[`\"']",
96
+ re.MULTILINE,
97
+ )
98
+ for match in import_pattern.finditer(content):
99
+ named_group = match.group(1)
100
+ default_imp = match.group(2)
101
+ src_mod = match.group(3)
102
+ if named_group:
103
+ for item in named_group.split(","):
104
+ sym = item.strip().split(" as ")[0].strip()
105
+ if sym:
106
+ imports.append({"module": src_mod, "symbol": sym})
107
+ elif default_imp:
108
+ imports.append({"module": src_mod, "symbol": default_imp.strip()})
109
+
110
+ # 2. Interfaces & Type Aliases
111
+ iface_pattern = re.compile(r"(?:export\s+)?interface\s+([A-Za-z0-9_]+)\s*\{([^}]*)\}", re.DOTALL)
112
+ for match in iface_pattern.finditer(content):
113
+ iname = match.group(1).strip()
114
+ ibody = match.group(2)
115
+ fields: Dict[str, Dict[str, Any]] = {}
116
+ for line in ibody.splitlines():
117
+ line_clean = line.strip().rstrip(";,")
118
+ if not line_clean or line_clean.startswith("//"):
119
+ continue
120
+ if ":" in line_clean:
121
+ fname, ftype = line_clean.split(":", 1)
122
+ fname_clean = fname.strip().rstrip("?")
123
+ fields[fname_clean] = {
124
+ "name": fname_clean,
125
+ "type": ftype.strip(),
126
+ "required": not fname.strip().endswith("?"),
127
+ }
128
+ iface_id = f"fe:{rel_file}:{iname}"
129
+ interfaces[iname] = {"name": iname, "fields": fields, "file": rel_file}
130
+ nodes.append({
131
+ "id": iface_id,
132
+ "type": "ts_interface",
133
+ "name": iname,
134
+ "fields": list(fields.keys()),
135
+ "file": rel_file,
136
+ "layer": "frontend",
137
+ })
138
+ edges.append({"source": module_id, "target": iface_id, "relation": "defines_interface"})
139
+
140
+ # 3. Functions, Arrow Functions, and React Components
141
+ fn_pattern = re.compile(
142
+ r"(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+([A-Za-z0-9_]+)\s*\(([^)]*)\)(?:\s*:\s*([A-Za-z0-9_<>[\]|&]+))?",
143
+ )
144
+ for match in fn_pattern.finditer(content):
145
+ fname = match.group(1).strip()
146
+ raw_params = match.group(2)
147
+ ret_type = (match.group(3) or "").strip()
148
+ params = _parse_ts_params(raw_params)
149
+ is_component = fname[0].isupper()
150
+ sym_id = f"fe:{rel_file}:{fname}"
151
+ symbols[fname] = {
152
+ "name": fname,
153
+ "kind": "react_component" if is_component else "ts_function",
154
+ "params": params,
155
+ "return_type": ret_type,
156
+ "signature_string": f"function {fname}({raw_params})" + (f": {ret_type}" if ret_type else ""),
157
+ "file": rel_file,
158
+ }
159
+ nodes.append({
160
+ "id": sym_id,
161
+ "type": "react_component" if is_component else "ts_function",
162
+ "name": fname,
163
+ "params": params,
164
+ "return_type": ret_type,
165
+ "file": rel_file,
166
+ "layer": "frontend",
167
+ })
168
+ edges.append({"source": module_id, "target": sym_id, "relation": "defines_symbol"})
169
+
170
+ arrow_pattern = re.compile(
171
+ r"(?:export\s+)?const\s+([A-Za-z0-9_]+)\s*(?::\s*[^=]+)?=\s*(?:async\s*)?\(([^)]*)\)\s*(?::\s*([A-Za-z0-9_<>[\]|&]+))?\s*=>",
172
+ )
173
+ for match in arrow_pattern.finditer(content):
174
+ fname = match.group(1).strip()
175
+ if fname in symbols:
176
+ continue
177
+ raw_params = match.group(2)
178
+ ret_type = (match.group(3) or "").strip()
179
+ params = _parse_ts_params(raw_params)
180
+ is_component = fname[0].isupper()
181
+ sym_id = f"fe:{rel_file}:{fname}"
182
+ symbols[fname] = {
183
+ "name": fname,
184
+ "kind": "react_component" if is_component else "ts_function",
185
+ "params": params,
186
+ "return_type": ret_type,
187
+ "signature_string": f"const {fname} = ({raw_params}) =>",
188
+ "file": rel_file,
189
+ }
190
+ nodes.append({
191
+ "id": sym_id,
192
+ "type": "react_component" if is_component else "ts_function",
193
+ "name": fname,
194
+ "params": params,
195
+ "return_type": ret_type,
196
+ "file": rel_file,
197
+ "layer": "frontend",
198
+ })
199
+ edges.append({"source": module_id, "target": sym_id, "relation": "defines_symbol"})
200
+
201
+ # 4. Property accesses on response/state objects (e.g. data.balance, user.balance, profile.email)
202
+ prop_pattern = re.compile(r"\b(?:data|user|res|response|payload|item|account|profile)\.([a-zA-Z_][a-zA-Z0-9_]*)\b")
203
+ for p_match in prop_pattern.finditer(content):
204
+ prop_name = p_match.group(1)
205
+ if prop_name not in ("json", "status", "ok", "headers", "then", "catch", "map", "filter", "length"):
206
+ if prop_name not in property_accesses:
207
+ property_accesses.append(prop_name)
208
+
209
+ # 5. API calls: fetch('/api/...'), axios.get('/api/...'), useSWR('/api/...')
210
+ fetch_pattern = re.compile(
211
+ r"\bfetch\s*\(\s*[`\"']([^`\"']+)[`\"'](?:\s*,\s*\{([^}]*)\})?",
212
+ re.DOTALL,
213
+ )
214
+ for match in fetch_pattern.finditer(content):
215
+ raw_url = match.group(1)
216
+ opts = match.group(2) or ""
217
+ m_match = re.search(r"method\s*:\s*[`\"']([A-Za-z]+)[`\"']", opts, re.IGNORECASE)
218
+ method = m_match.group(1).upper() if m_match else "GET"
219
+ norm_path = _normalize_url_to_route_path(raw_url)
220
+ api_calls.append({
221
+ "client": "fetch",
222
+ "method": method,
223
+ "raw_url": raw_url,
224
+ "normalized_path": norm_path,
225
+ "accessed_fields": property_accesses,
226
+ "file": rel_file,
227
+ })
228
+
229
+ axios_pattern = re.compile(
230
+ r"\baxios\.(get|post|put|delete|patch)\s*(?:<[^>]+>)?\s*\(\s*[`\"']([^`\"']+)[`\"']",
231
+ re.IGNORECASE,
232
+ )
233
+ for match in axios_pattern.finditer(content):
234
+ method = match.group(1).upper()
235
+ raw_url = match.group(2)
236
+ norm_path = _normalize_url_to_route_path(raw_url)
237
+ api_calls.append({
238
+ "client": "axios",
239
+ "method": method,
240
+ "raw_url": raw_url,
241
+ "normalized_path": norm_path,
242
+ "accessed_fields": property_accesses,
243
+ "file": rel_file,
244
+ })
245
+
246
+ swr_pattern = re.compile(r"\b(?:useSWR|useQuery)\s*(?:<[^>]+>)?\s*\(\s*[`\"'](/[^`\"']+)[`\"']")
247
+ for match in swr_pattern.finditer(content):
248
+ raw_url = match.group(1)
249
+ norm_path = _normalize_url_to_route_path(raw_url)
250
+ api_calls.append({
251
+ "client": "hook",
252
+ "method": "GET",
253
+ "raw_url": raw_url,
254
+ "normalized_path": norm_path,
255
+ "accessed_fields": property_accesses,
256
+ "file": rel_file,
257
+ })
258
+
259
+ return {
260
+ "file": rel_file,
261
+ "symbols": symbols,
262
+ "interfaces": interfaces,
263
+ "imports": imports,
264
+ "api_calls": api_calls,
265
+ "property_accesses": property_accesses,
266
+ "nodes": nodes,
267
+ "edges": edges,
268
+ }
269
+
270
+
271
+ def parse_ts_react_file(file_path: str, repo_root: str = "") -> Dict[str, Any]:
272
+ if not os.path.exists(file_path):
273
+ return {
274
+ "error": f"Frontend file not found: {file_path}",
275
+ "symbols": {},
276
+ "interfaces": {},
277
+ "imports": [],
278
+ "api_calls": [],
279
+ "property_accesses": [],
280
+ "nodes": [],
281
+ "edges": [],
282
+ }
283
+ rel_file = os.path.relpath(file_path, repo_root).replace("\\", "/") if repo_root else os.path.basename(file_path)
284
+ try:
285
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
286
+ content = f.read()
287
+ except Exception as exc:
288
+ return {
289
+ "error": str(exc),
290
+ "symbols": {},
291
+ "interfaces": {},
292
+ "imports": [],
293
+ "api_calls": [],
294
+ "property_accesses": [],
295
+ "nodes": [],
296
+ "edges": [],
297
+ }
298
+ return parse_ts_react_content(content, rel_file)
299
+
@@ -0,0 +1,175 @@
1
+ """
2
+ BlastTrace Vue.js & Nuxt 3 AST Parser
3
+ Extracts component definitions, script interfaces, Pinia state bindings,
4
+ template property references, and HTTP / useFetch / $fetch API route queries.
5
+ """
6
+
7
+ import os
8
+ import re
9
+ from typing import Dict, Any, List, Optional, Set
10
+ from dataclasses import dataclass, field
11
+
12
+
13
+ @dataclass
14
+ class VueApiCall:
15
+ endpoint: str
16
+ method: str = "GET"
17
+ line_number: int = 1
18
+ response_fields_accessed: List[str] = field(default_factory=list)
19
+
20
+
21
+ @dataclass
22
+ class VueComponent:
23
+ name: str
24
+ file_path: str
25
+ props: List[str] = field(default_factory=list)
26
+ interfaces: List[str] = field(default_factory=list)
27
+ state_properties: List[str] = field(default_factory=list)
28
+ template_properties: List[str] = field(default_factory=list)
29
+ api_calls: List[VueApiCall] = field(default_factory=list)
30
+
31
+
32
+ @dataclass
33
+ class VueParseResult:
34
+ components: List[VueComponent] = field(default_factory=list)
35
+ api_calls: List[VueApiCall] = field(default_factory=list)
36
+ interfaces: Dict[str, List[str]] = field(default_factory=dict)
37
+ all_accessed_properties: Set[str] = field(default_factory=set)
38
+
39
+
40
+ # Regex patterns for Vue 2/3 and Nuxt 3
41
+ _RE_SCRIPT = re.compile(r"<script[^>]*>(.*?)</script>", re.DOTALL | re.IGNORECASE)
42
+ _RE_TEMPLATE = re.compile(r"<template[^>]*>(.*?)</template>", re.DOTALL | re.IGNORECASE)
43
+
44
+ # TypeScript interfaces inside Vue script
45
+ _RE_INTERFACE = re.compile(r"(?:export\s+)?interface\s+([A-Za-z0-9_]+)\s*\{([^}]*)\}", re.DOTALL)
46
+ _RE_TYPE_ALIAS = re.compile(r"(?:export\s+)?type\s+([A-Za-z0-9_]+)\s*=\s*\{([^}]*)\}", re.DOTALL)
47
+
48
+ # Vue defineProps
49
+ _RE_DEFINE_PROPS = re.compile(r"defineProps<\s*\{([^}]*)\}\s*>", re.DOTALL)
50
+ _RE_DEFINE_PROPS_ARR = re.compile(r"defineProps\(\s*\[([^\]]+)\]\s*\)")
51
+
52
+ # Pinia stores: const store = useProductStore()
53
+ _RE_PINIA = re.compile(r"(?:const|let|var)\s+([A-Za-z0-9_]+)\s*=\s*(use[A-Za-z0-9_]+Store)\(\)")
54
+
55
+ # API Fetching in Vue / Nuxt:
56
+ # 1. Nuxt: useFetch('/api/v1/user') or useAsyncData('key', () => $fetch('/api/...'))
57
+ # 2. $fetch('/api/v1/user')
58
+ # 3. axios.get('/api/v1/user')
59
+ # 4. fetch('/api/v1/user')
60
+ _RE_NUXT_USE_FETCH = re.compile(r"useFetch(?:<[^>]+>)?\(\s*['\"`](/[^'\"`]+)['\"`]")
61
+ _RE_DOLLAR_FETCH = re.compile(r"\$fetch(?:<[^>]+>)?\(\s*['\"`](/[^'\"`]+)['\"`]")
62
+ _RE_AXIOS = re.compile(r"axios\.(get|post|put|delete|patch)\(\s*['\"`](/[^'\"`]+)['\"`]", re.IGNORECASE)
63
+ _RE_STANDARD_FETCH = re.compile(r"fetch\(\s*['\"`](/[^'\"`]+)['\"`]")
64
+
65
+ # Template property accesses: {{ item.price }}, :value="user.balance", v-if="order.tax"
66
+ _RE_TEMPLATE_INTERPOLATION = re.compile(r"\{\{\s*([^}]+)\s*\}\}")
67
+ _RE_TEMPLATE_ATTR_BINDING = re.compile(r"(?::|v-bind:|v-model|v-if|v-for=)[a-zA-Z0-9_\-\.]*=['\"]([^'\"]+)['\"]")
68
+ _RE_PROP_ACCESS = re.compile(r"(?:[A-Za-z0-9_]+)\.([A-Za-z0-9_]+)")
69
+
70
+
71
+ def parse_vue_content(content: str, file_path: str = "Component.vue") -> VueParseResult:
72
+ """Parses single-file Vue / Nuxt component (.vue)."""
73
+ result = VueParseResult()
74
+ basename = os.path.basename(file_path)
75
+ comp_name = os.path.splitext(basename)[0]
76
+
77
+ component = VueComponent(name=comp_name, file_path=file_path)
78
+
79
+ # 1. Extract Scripts
80
+ scripts = _RE_SCRIPT.findall(content)
81
+ script_text = "\n".join(scripts) if scripts else ""
82
+
83
+ # Parse Interfaces
84
+ for match in _RE_INTERFACE.finditer(script_text):
85
+ ifname = match.group(1)
86
+ body = match.group(2)
87
+ fields = [f.split(":")[0].strip().replace("?", "") for f in body.split(";") if ":" in f]
88
+ component.interfaces.append(ifname)
89
+ result.interfaces[ifname] = fields
90
+
91
+ for match in _RE_TYPE_ALIAS.finditer(script_text):
92
+ tname = match.group(1)
93
+ body = match.group(2)
94
+ fields = [f.split(":")[0].strip().replace("?", "") for f in body.split(";") if ":" in f]
95
+ component.interfaces.append(tname)
96
+ result.interfaces[tname] = fields
97
+
98
+ # Parse defineProps
99
+ for match in _RE_DEFINE_PROPS.finditer(script_text):
100
+ body = match.group(1)
101
+ props = [p.split(":")[0].strip().replace("?", "") for p in body.split(";") if ":" in p]
102
+ component.props.extend(props)
103
+
104
+ for match in _RE_DEFINE_PROPS_ARR.finditer(script_text):
105
+ raw_props = match.group(1)
106
+ props = [p.strip().strip("'\"") for p in raw_props.split(",") if p.strip()]
107
+ component.props.extend(props)
108
+
109
+ # Parse API calls
110
+ # Nuxt useFetch
111
+ for match in _RE_NUXT_USE_FETCH.finditer(script_text):
112
+ endpoint = match.group(1)
113
+ call = VueApiCall(endpoint=endpoint, method="GET")
114
+ component.api_calls.append(call)
115
+ result.api_calls.append(call)
116
+
117
+ # $fetch
118
+ for match in _RE_DOLLAR_FETCH.finditer(script_text):
119
+ endpoint = match.group(1)
120
+ call = VueApiCall(endpoint=endpoint, method="GET")
121
+ component.api_calls.append(call)
122
+ result.api_calls.append(call)
123
+
124
+ # Axios
125
+ for match in _RE_AXIOS.finditer(script_text):
126
+ method = match.group(1).upper()
127
+ endpoint = match.group(2)
128
+ call = VueApiCall(endpoint=endpoint, method=method)
129
+ component.api_calls.append(call)
130
+ result.api_calls.append(call)
131
+
132
+ # Standard fetch
133
+ for match in _RE_STANDARD_FETCH.finditer(script_text):
134
+ endpoint = match.group(1)
135
+ call = VueApiCall(endpoint=endpoint, method="GET")
136
+ component.api_calls.append(call)
137
+ result.api_calls.append(call)
138
+
139
+ # 2. Extract Templates
140
+ templates = _RE_TEMPLATE.findall(content)
141
+ template_text = "\n".join(templates) if templates else ""
142
+
143
+ # Check property bindings in template
144
+ accessed: Set[str] = set()
145
+ for match in _RE_TEMPLATE_INTERPOLATION.finditer(template_text):
146
+ expr = match.group(1)
147
+ for pmatch in _RE_PROP_ACCESS.finditer(expr):
148
+ accessed.add(pmatch.group(1))
149
+
150
+ for match in _RE_TEMPLATE_ATTR_BINDING.finditer(template_text):
151
+ expr = match.group(1)
152
+ for pmatch in _RE_PROP_ACCESS.finditer(expr):
153
+ accessed.add(pmatch.group(1))
154
+
155
+ # Also extract property accesses in script
156
+ for pmatch in _RE_PROP_ACCESS.finditer(script_text):
157
+ accessed.add(pmatch.group(1))
158
+
159
+ component.template_properties = sorted(list(accessed))
160
+ result.all_accessed_properties = accessed
161
+ result.components.append(component)
162
+
163
+ return result
164
+
165
+
166
+ def parse_vue_file(file_path: str) -> Optional[VueParseResult]:
167
+ """Reads and parses a .vue file."""
168
+ if not os.path.exists(file_path):
169
+ return None
170
+ try:
171
+ with open(file_path, "r", encoding="utf-8") as f:
172
+ content = f.read()
173
+ return parse_vue_content(content, file_path)
174
+ except Exception:
175
+ return None