cg-code-graph 0.10.1__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.
Files changed (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. codegraph/xcode.py +251 -0
File without changes
@@ -0,0 +1,336 @@
1
+ """Next.js framework layer (on the TypeScript plugin).
2
+
3
+ app router app/**/page.* -> page node (route in Next notation, e.g. /books/[id]; entry ui_page) RENDERS the default
4
+ export; layout/template/error/loading/not-found/default -> layout node (ui_global), pages USES_LAYOUT every
5
+ enclosing layout; route groups (x), parallel slots @x and private _folders are dropped, intercepting
6
+ routes (.)x (..)x (...)x are resolved to the URL they intercept (best effort)
7
+ app/**/route.* -> one route per exported HTTP verb (GET/POST/...; `export const GET = withAuth(fn)` and
8
+ `export { h as GET }` resolve to the wrapped function); dynamic segments [id] -> {id}, [...slug] ->
9
+ {slug*}, [[...slug]] -> {slug*?}
10
+ pages router pages/**/*.tsx -> page (ui_page; getServerSideProps/getStaticProps CALLS), _app/_document -> ui_global,
11
+ pages/api/** -> route per `req.method === 'X'` / switch case, else ANY
12
+ server actions 'use server' modules (exported functions) and inline 'use server' functions -> route
13
+ `ACTION <file>#<fn>` (entry http_route, method POST), clients reach them through ordinary CALLS
14
+ middleware middleware.ts / proxy.ts: USES_MIDDLEWARE from every route/page its `config.matcher` covers (regex matchers
15
+ and no matcher: all, heuristic)
16
+ next.config static basePath (prefixed to every route/page) and literal rewrites (recorded; internal destinations are
17
+ added as uri variants of the destination route)
18
+ links fetch/axios/ky/SWR calls in the same repo -> route handlers (MATCHES_ROUTE, see tsweb.common.link_in_repo);
19
+ env keys incl. NEXT_PUBLIC_ (tsweb.data)
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import re
24
+ from pathlib import PurePosixPath
25
+
26
+ from ...core.plugin import FrameworkPlugin, GraphBuilder, Project
27
+ from ..tsweb.common import (HTTP_VERBS, TEST_SKIP_RE, add_route, express_path, finish, fw_facts, has_server_framework,
28
+ join_path, merge_extractor_cfg, module_of, next_segment, obj, ref_nodes, register, sval, svals)
29
+
30
+ EXT = r"\.(tsx|ts|jsx|js|mdx)$"
31
+ SPECIAL = {"layout", "template", "error", "loading", "not-found", "default", "global-error", "forbidden", "unauthorized"}
32
+ CONFIG_FILES = ("next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs")
33
+
34
+
35
+ class NextPlugin(FrameworkPlugin):
36
+ name, language = "nextjs", "typescript"
37
+
38
+ def detect(self, project: Project) -> bool:
39
+ return has_server_framework(project, "next") or any(project.exists(f) for f in CONFIG_FILES)
40
+
41
+ def register_hooks(self, ctx) -> None:
42
+ root = ctx.project.root
43
+ merge_extractor_cfg(ctx.extractor_cfg, src_dirs=["."], skip_re=TEST_SKIP_RE + r"|(^|/)(public|scripts)(/|$)",
44
+ extra_files=[f for f in CONFIG_FILES if (root / f).exists()] +
45
+ [f for f in ("middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js", "proxy.ts", "src/proxy.ts") if (root / f).exists()],
46
+ walk_src=not (root / "tsconfig.json").exists())
47
+ register(ctx, self.name)
48
+
49
+ # ------------------------------------------------------------
50
+ def contribute(self, project: Project, b: GraphBuilder, ctx) -> dict:
51
+ st = {"pages": 0, "layouts": 0, "route_handlers": 0, "api_routes": 0, "server_actions": 0, "middleware": None,
52
+ "base_path": None, "rewrites": 0}
53
+ F = fw_facts(ctx)
54
+ if not F:
55
+ return {**st, "status": "no facts"}
56
+ self.b, self.F = b, F
57
+ mods = F.get("modules") or {}
58
+ self.mods = mods
59
+ root = project.root
60
+ app_dirs = [d for d in ("app", "src/app") if (root / d).is_dir()]
61
+ page_dirs = [d for d in ("pages", "src/pages") if (root / d).is_dir()]
62
+ base_path, rewrites = self._config(mods)
63
+ st["base_path"], st["rewrites"] = base_path, len(rewrites)
64
+ self.base = base_path or ""
65
+ files = sorted({n.file for n in b.nodes.values() if n.lang == "ts" and n.kind == "module" and n.file})
66
+ self.routes, self.pages = [], []
67
+ layouts_by_dir = {}
68
+ for f in files:
69
+ for ad in app_dirs:
70
+ if not f.startswith(ad + "/") or not re.search(EXT, f):
71
+ continue
72
+ stem = re.sub(EXT, "", PurePosixPath(f).name)
73
+ rel_dir = str(PurePosixPath(f).parent)[len(ad):].strip("/")
74
+ url = self._app_url(rel_dir)
75
+ if url is None:
76
+ continue
77
+ if stem == "page":
78
+ self._page(f, url, "app", st)
79
+ elif stem == "route":
80
+ self._route_handlers(f, url, st)
81
+ elif stem in SPECIAL:
82
+ lid = self._layout(f, stem, st)
83
+ layouts_by_dir.setdefault(str(PurePosixPath(f).parent), []).append(lid)
84
+ for pd in page_dirs:
85
+ if not f.startswith(pd + "/") or not re.search(EXT, f):
86
+ continue
87
+ rel = re.sub(EXT, "", f[len(pd) + 1:])
88
+ parts = rel.split("/")
89
+ if parts[0] == "api":
90
+ url = self._pages_url(parts)
91
+ if url is not None:
92
+ self._api_route(f, url, st)
93
+ elif parts[-1] in ("_app", "_document", "_error") and len(parts) == 1:
94
+ lid = self._layout(f, parts[-1], st)
95
+ layouts_by_dir.setdefault(pd, []).append(lid)
96
+ elif not any(p.startswith("_") for p in parts):
97
+ url = self._pages_url(parts)
98
+ if url is not None:
99
+ self._page(f, url, "pages", st)
100
+ # pages use every enclosing layout (app router) / _app (pages router)
101
+ for pid, f in self.pages:
102
+ d = PurePosixPath(f).parent
103
+ while True:
104
+ for lid in layouts_by_dir.get(str(d), []):
105
+ if lid != pid:
106
+ b.add_edge(pid, lid, "USES_LAYOUT", file=f, line=1, confidence="exact")
107
+ if str(d) in app_dirs + page_dirs or str(d) in (".", ""):
108
+ break
109
+ d = d.parent
110
+ self._server_actions(st)
111
+ self._middleware(st)
112
+ for src, dst in rewrites:
113
+ for r in self.routes:
114
+ n = b.nodes[r]
115
+ if n.attrs.get("uri") == express_path(self.base + dst) and not re.search(r"^https?:", dst):
116
+ n.attrs.setdefault("uri_variants", []).append(express_path(self.base + src))
117
+ n.attrs.setdefault("rewrites", []).append({"source": src, "destination": dst})
118
+ st.update(finish(project, b, ctx, self.name))
119
+ return st
120
+
121
+ # ------------------------------------------------------------ paths
122
+ def _app_url(self, rel_dir: str) -> str | None:
123
+ segs = []
124
+ for s in [x for x in rel_dir.split("/") if x]:
125
+ m = re.match(r"^(\((?:\.{1,3}|\.\.\)\(\.\.)\))(.*)$", s) # intercepting route
126
+ if m:
127
+ marker, rest = m.group(1), m.group(2)
128
+ if marker == "(.)":
129
+ pass
130
+ elif marker == "(...)":
131
+ segs = []
132
+ else:
133
+ ups = marker.count("..")
134
+ segs = segs[:max(0, len(segs) - ups)]
135
+ s = rest
136
+ ns = next_segment(s)
137
+ if ns is None:
138
+ return None
139
+ if ns:
140
+ segs.append(ns)
141
+ return "/" + "/".join(segs)
142
+
143
+ def _pages_url(self, parts: list[str]) -> str | None:
144
+ segs = []
145
+ for i, p in enumerate(parts):
146
+ if p == "index" and i == len(parts) - 1:
147
+ continue
148
+ ns = next_segment(p)
149
+ if ns is None:
150
+ return None
151
+ if ns:
152
+ segs.append(ns)
153
+ return "/" + "/".join(segs)
154
+
155
+ @staticmethod
156
+ def _page_name(url: str) -> str:
157
+ return re.sub(r"\{(\w+)\*\?\}", r"[[...\1]]", re.sub(r"\{(\w+)\*\}", r"[...\1]", re.sub(r"\{(\w+)\}", r"[\1]", url)))
158
+
159
+ def _exports(self, f) -> dict:
160
+ return {e["name"]: e for e in (self.mods.get(f) or {}).get("exports") or []}
161
+
162
+ def _targets(self, ex: dict | None) -> tuple[list[str], str]:
163
+ if not ex:
164
+ return [], "exact"
165
+ if ex.get("node") and self.b.has(ex["node"]):
166
+ return [ex["node"]], "exact"
167
+ nodes = [n for n in ref_nodes(ex.get("desc")) if self.b.has(n)]
168
+ return nodes, "resolved"
169
+
170
+ # ------------------------------------------------------------ nodes
171
+ def _page(self, f, url, router, st):
172
+ b = self.b
173
+ name = self._page_name(join_path(self.base, url) if self.base else url)
174
+ pid = b.add_node("page", f, name=name, fqn=name, file=f, line=1, module=module_of(f), lang="ts",
175
+ attrs={"route": name, "uri": express_path(join_path(self.base, url)), "framework": "nextjs", "router": router})
176
+ b.nodes[pid].entry_kind = "ui_page"
177
+ ex = self._exports(f)
178
+ tg, conf = self._targets(ex.get("default"))
179
+ for t in tg:
180
+ b.add_edge(pid, t, "RENDERS", file=f, line=b.nodes[t].line or 1, confidence=conf)
181
+ b.add_edge(pid, f"module:{f}", "RENDERS", file=f, line=1, confidence="exact") if not tg and b.has(f"module:{f}") else None
182
+ for data_fn in ("getServerSideProps", "getStaticProps", "getStaticPaths", "generateMetadata", "generateStaticParams", "getInitialProps"):
183
+ tg2, c2 = self._targets(ex.get(data_fn))
184
+ for t in tg2:
185
+ b.add_edge(pid, t, "CALLS", file=f, line=b.nodes[t].line or 1, confidence=c2, data_fetching=data_fn)
186
+ self.pages.append((pid, f))
187
+ st["pages"] += 1
188
+
189
+ def _layout(self, f, stem, st):
190
+ b = self.b
191
+ lid = b.add_node("layout", f, name=f, file=f, line=1, module=module_of(f), lang="ts", attrs={"framework": "nextjs", "next_kind": stem})
192
+ b.nodes[lid].entry_kind = "ui_global"
193
+ tg, conf = self._targets(self._exports(f).get("default"))
194
+ for t in tg:
195
+ b.add_edge(lid, t, "RENDERS", file=f, line=b.nodes[t].line or 1, confidence=conf)
196
+ st["layouts"] += 1
197
+ return lid
198
+
199
+ def _route_handlers(self, f, url, st):
200
+ ex = self._exports(f)
201
+ uri = express_path(join_path(self.base, url))
202
+ for verb in HTTP_VERBS:
203
+ if verb not in ex:
204
+ continue
205
+ tg, conf = self._targets(ex[verb])
206
+ e = ex[verb]
207
+ attrs = {"file_route": True}
208
+ d = e.get("desc") or {}
209
+ if "call" in d:
210
+ attrs["wrapped_by"] = d["call"]
211
+ if not tg:
212
+ attrs["handler_unresolved"] = True
213
+ mw = [(n, d["call"], "resolved") for n in [d.get("node")] if n and "call" in d and self.b.has(n)]
214
+ rid = add_route(self.b, verb, uri, tg, f, e.get("line") or 1, "nextjs", conf, attrs, mw)
215
+ self.routes.append(rid)
216
+ st["route_handlers"] += 1
217
+
218
+ def _api_route(self, f, url, st):
219
+ ex = self._exports(f)
220
+ tg, conf = self._targets(ex.get("default"))
221
+ checks = (self.mods.get(f) or {}).get("method_checks") or []
222
+ methods = sorted({c["method"] for c in checks}) or ["ANY"]
223
+ uri = express_path(join_path(self.base, "/api", url[len("/api"):] if url.startswith("/api") else url))
224
+ d = (ex.get("default") or {}).get("desc") or {}
225
+ mw = [(n, d["call"], "resolved") for n in [d.get("node")] if n and "call" in d and self.b.has(n)]
226
+ for m in methods:
227
+ rid = add_route(self.b, m, uri, tg, f, (ex.get("default") or {}).get("line") or 1, "nextjs", conf,
228
+ {"pages_api": True, "method_from": "req.method check" if checks else "any"}, mw)
229
+ self.routes.append(rid)
230
+ st["api_routes"] += 1
231
+
232
+ def _server_actions(self, st):
233
+ b = self.b
234
+ seen = set()
235
+ for f, m in self.mods.items():
236
+ fns = list(m.get("server_fns") or [])
237
+ if "use server" in (m.get("directives") or []):
238
+ fns += [e["node"] for e in m.get("exports") or [] if e.get("node") and e["node"].startswith("function:")]
239
+ for fn in fns:
240
+ if fn in seen or not b.has(fn):
241
+ continue
242
+ seen.add(fn)
243
+ n = b.nodes[fn]
244
+ key = f"ACTION {fn.split(':', 1)[1]}"
245
+ rid = b.add_node("route", key, name=key, file=n.file, line=n.line, module=module_of(n.file), lang="ts", entry_kind="http_route",
246
+ attrs={"method": "POST", "server_action": True, "framework": "nextjs", "handler": n.name})
247
+ b.add_edge(rid, fn, "ROUTES_TO", file=n.file, line=n.line, confidence="exact")
248
+ st["server_actions"] += 1
249
+
250
+ def _middleware(self, st):
251
+ b = self.b
252
+ for f in ("middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js", "proxy.ts", "proxy.js", "src/proxy.ts"):
253
+ m = self.mods.get(f)
254
+ if not m:
255
+ continue
256
+ ex = {e["name"]: e for e in m.get("exports") or []}
257
+ fn = (ex.get("middleware") or ex.get("proxy") or ex.get("default") or {}).get("node")
258
+ if not fn or not b.has(fn):
259
+ tg = ref_nodes((ex.get("default") or {}).get("desc"))
260
+ fn = tg[0] if tg else None
261
+ if not fn:
262
+ continue
263
+ cfg = obj((ex.get("config") or {}).get("desc"))
264
+ matchers = svals(cfg.get("matcher")) or [sval(obj(x).get("source")) for x in (cfg.get("matcher") or {}).get("arr") or [] if sval(obj(x).get("source"))]
265
+ regex = [p for p in matchers if re.search(r"[()|\\^$]", p)]
266
+ pats = [p for p in matchers if p not in regex]
267
+ st["middleware"] = {"file": f, "matchers": matchers}
268
+ targets = [(r, b.nodes[r].attrs.get("uri")) for r in self.routes] + [(p, b.nodes[p].attrs.get("uri")) for p, _ in self.pages]
269
+ for nid, uri in targets:
270
+ if not uri:
271
+ continue
272
+ if not matchers or regex:
273
+ hit, conf = (not regex or self._regex_hit(regex, uri)), "heuristic"
274
+ else:
275
+ hit, conf = any(self._matcher_hit(p, uri) for p in pats), "resolved"
276
+ if hit:
277
+ b.add_edge(nid, fn, "USES_MIDDLEWARE", file=f, line=b.nodes[fn].line or 1, confidence=conf, name="middleware")
278
+
279
+ def _matcher_hit(self, pat: str, uri: str) -> bool:
280
+ p = express_path(pat)
281
+ ps, us = [s for s in p.split("/") if s], [s for s in uri.split("/") if s]
282
+ for i, s in enumerate(ps):
283
+ if re.fullmatch(r"\{\w+\*\??\}", s):
284
+ return len(us) >= i + (0 if s.endswith("?}") else 1)
285
+ if i >= len(us):
286
+ return bool(re.fullmatch(r"\{\w+\?\}", s))
287
+ if s.startswith("{") or us[i].startswith("{") or s == us[i]:
288
+ continue
289
+ return False
290
+ return len(us) == len(ps)
291
+
292
+ @staticmethod
293
+ def _regex_hit(regex: list[str], uri: str) -> bool:
294
+ probe = re.sub(r"\{(\w+)\*?\??\}", r"x", uri)
295
+ for r in regex:
296
+ try:
297
+ if re.fullmatch(r.replace("/:path*", "(/.*)?"), probe) or re.match(r, probe):
298
+ return True
299
+ except re.error:
300
+ return True
301
+ return False
302
+
303
+ def _config(self, mods):
304
+ base, rewrites = None, []
305
+ for f in CONFIG_FILES:
306
+ m = mods.get(f)
307
+ if not m:
308
+ continue
309
+ ex = {e["name"]: e for e in m.get("exports") or []}
310
+ d = (ex.get("default") or ex.get("export=") or {}).get("desc") or {}
311
+ o = self._config_obj(d)
312
+ base = sval(o.get("basePath")) or None
313
+ rw = o.get("rewrites") or {}
314
+ for x in (rw.get("ret") or [rw.get("body")] if (rw.get("ret") or rw.get("body")) else []):
315
+ for it in (x or {}).get("arr") or []:
316
+ s, t = sval(obj(it).get("source")), sval(obj(it).get("destination"))
317
+ if s and t:
318
+ rewrites.append((s, t))
319
+ return base, rewrites
320
+
321
+ def _config_obj(self, d, depth=0) -> dict:
322
+ if not isinstance(d, dict) or depth > 4:
323
+ return {}
324
+ if d.get("obj") is not None:
325
+ return d["obj"]
326
+ if d.get("key"):
327
+ inst = (self.F.get("instances") or {}).get(d["key"]) or {}
328
+ return self._config_obj(inst.get("init"), depth + 1)
329
+ if "call" in d: # withPlugins(nextConfig) / withBundleAnalyzer({...})
330
+ for a in d.get("args") or []:
331
+ o = self._config_obj(a, depth + 1)
332
+ if o:
333
+ return o
334
+ if d.get("body") or d.get("ret"):
335
+ return self._config_obj(d.get("body") or (d.get("ret") or [None])[0], depth + 1)
336
+ return {}
File without changes
@@ -0,0 +1,308 @@
1
+ """Nuxt framework plugin (on the TypeScript plugin).
2
+
3
+ register_hooks: finds the source dir (nuxt.config `srcDir`, else `app/` (Nuxt 4), `src/`, or the repo root
4
+ (Nuxt 3 default)) and points the TS extractor at Nuxt's generated types (.nuxt/tsconfig.app.json or
5
+ .nuxt/tsconfig.json, whose nuxt.d.ts pulls in .nuxt/types/imports.d.ts = auto-imported composables /
6
+ utils / stores) and the generated global-components map (.nuxt/types/components.d.ts), plus Nuxt
7
+ directory conventions (pages/, layouts/, components/, composables/, stores/, utils/, middleware/, plugins/).
8
+ A clean checkout without .nuxt (no `nuxi prepare` run) gets an equivalent generated on the fly in a temp
9
+ dir: path aliases (~, @, ~~, @@), auto-imports of composables/ utils/ stores/ exports, global components
10
+ with Nuxt's path-prefixed names. Vue / Nuxt built-ins resolve when node_modules is installed. The stats
11
+ say which one was used (`prepared`: nuxi | generated).
12
+ contribute: file-based routing (route path per page), entry points (pages = ui_page; app.vue,
13
+ layouts, global middleware, plugins = ui_global), USES_LAYOUT, i18n keys (USES_I18N; defining
14
+ locale files).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import atexit
19
+ import json
20
+ import re
21
+ import shutil
22
+ import sys
23
+ import tempfile
24
+ from pathlib import Path
25
+
26
+ from ... import presets
27
+ from ...core.plugin import FrameworkPlugin, GraphBuilder, Project
28
+
29
+ # dependency / VCS directories every walk skips (codegraph/presets/common.yaml skip_dirs)
30
+ COMMON_SKIP = presets.skip_dirs("common")
31
+
32
+ DIR_KINDS = [("pages/", "page"), ("layouts/", "layout"), ("components/", "component"), ("composables/", "composable"),
33
+ ("stores/", "store"), ("utils/", "util"), ("middleware/", "middleware"), ("plugins/", "plugin"),
34
+ ("app.vue", "app"), ("error.vue", "app")]
35
+
36
+
37
+ def kind_rules(src_dir: str = "app") -> list[tuple[str, str]]:
38
+ pre = "" if src_dir in ("", ".") else src_dir.strip("/") + "/"
39
+ return [(pre + d, k) for d, k in DIR_KINDS]
40
+
41
+
42
+ KIND_RULES = kind_rules("app")
43
+
44
+
45
+ def nuxt_route(rel: str, src_dir: str = "app") -> str:
46
+ """app/pages/shelves/[id]/books/[bookId].vue -> /shelves/:id/books/:bookId (pages/... when srcDir is the root)"""
47
+ pre = "" if src_dir in ("", ".") else src_dir.strip("/") + "/"
48
+ p = rel[len(f"{pre}pages/"):]
49
+ p = re.sub(r"\.vue$", "", p)
50
+ segs = []
51
+ parts = p.split("/")
52
+ for i, s in enumerate(parts):
53
+ if re.fullmatch(r"\(.*\)", s):
54
+ continue # route group
55
+ if s == "index" and i == len(parts) - 1:
56
+ continue
57
+ s = re.sub(r"\[\.\.\.(\w+)\]", r":\1(.*)*", s)
58
+ s = re.sub(r"\[\[(\w+)\]\]", r":\1?", s)
59
+ s = re.sub(r"\[(\w+)\]", r":\1", s)
60
+ segs.append(s)
61
+ return "/" + "/".join(segs)
62
+
63
+
64
+ def find_src_dir(root: Path) -> str:
65
+ """nuxt.config srcDir, else app/ (Nuxt 4 layout), src/ (when it holds pages/ or app.vue), else the root."""
66
+ for n in ("nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"):
67
+ f = root / n
68
+ if f.exists():
69
+ m = re.search(r"\bsrcDir\s*:\s*['\"`]([^'\"`]+)['\"`]", f.read_text(errors="replace"))
70
+ if m:
71
+ d = m.group(1).strip().lstrip("./").rstrip("/") or "."
72
+ if (root / d).is_dir():
73
+ return d
74
+ for d in ("app", "src"):
75
+ if any((root / d / x).exists() for x in ("pages", "app.vue", "components", "layouts", "composables")):
76
+ return d
77
+ if (root / "app").is_dir() and not (root / "pages").exists():
78
+ return "app"
79
+ return "."
80
+
81
+
82
+ AUTO_IMPORT_DIRS = ("composables", "utils", "stores")
83
+ # kept out of the generated tsconfig: build output, native shells, static assets (codegraph/presets/nuxt.yaml)
84
+ EXCLUDE = sorted(presets.skip_dirs("nuxt"))
85
+
86
+
87
+ def _exports(f: Path) -> list[tuple[str, str]]:
88
+ """(local name, export name) pairs of a module, by regex (enough for auto-import declarations)."""
89
+ txt = f.read_text(errors="replace")
90
+ out = []
91
+ for m in re.finditer(r"^export\s+(?:async\s+)?(?:function\*?|const|let|var|class)\s+([A-Za-z_$][\w$]*)", txt, re.M):
92
+ out.append((m.group(1), m.group(1)))
93
+ for m in re.finditer(r"^export\s*\{([^}]*)\}(?!\s*from)", txt, re.M):
94
+ for part in m.group(1).split(","):
95
+ bits = part.strip().split(" as ")
96
+ if bits[0].strip() and not bits[0].strip().startswith("type "):
97
+ out.append((bits[-1].strip(), bits[-1].strip()))
98
+ if re.search(r"^export\s+default\b", txt, re.M):
99
+ stem = f.stem if f.stem != "index" else f.parent.name
100
+ name = re.sub(r"[-_.](\w)", lambda m: m.group(1).upper(), stem)
101
+ out.append((name[:1].lower() + name[1:], "default"))
102
+ return out
103
+
104
+
105
+ def _split_case(s: str) -> list[str]:
106
+ """scule's splitByCase: `BaseButton` / `base-button` / `base_button` -> [Base, Button] / [base, button]."""
107
+ out = []
108
+ for chunk in re.split(r"[-_./]", s):
109
+ out += [w for w in re.findall(r"[A-Z]+(?=[A-Z][a-z0-9])|[A-Z]?[a-z0-9]+|[A-Z]+", chunk) if w]
110
+ return out
111
+
112
+
113
+ def _component_name(rel_parts: list[str]) -> str:
114
+ """Nuxt's component name for a file under components/ (resolveComponentNameSegments): the directory words, minus
115
+ the trailing ones the file name already starts with. base/form/Input.vue -> BaseFormInput, base/BaseButton.vue ->
116
+ BaseButton, list/Lists.vue -> ListLists, form/input/index.vue -> FormInput, Foo.client.vue -> Foo."""
117
+ *dirs, file = rel_parts
118
+ stem = re.sub(r"(\.(client|server))?\.(vue|tsx|jsx|ts|js)$", "", file)
119
+ file_parts = [] if stem.lower() == "index" and dirs else _split_case(stem)
120
+ content = "/".join(file_parts).lower()
121
+ name_parts = [w for d in dirs for w in _split_case(d)]
122
+ cut, suffix = None, []
123
+ for i in range(len(dirs) - 1, -1, -1):
124
+ suffix = [w.lower() for w in _split_case(dirs[i])] + suffix
125
+ sc = "/".join(suffix)
126
+ if content and (content == sc or content.startswith(sc + "/")):
127
+ cut = sum(len(_split_case(d)) for d in dirs[:i])
128
+ if cut is not None:
129
+ name_parts = name_parts[:cut]
130
+ return "".join(w[:1].upper() + w[1:] for w in name_parts + file_parts)
131
+
132
+
133
+ def generate_types(root: Path, src_dir: str) -> tuple[Path, dict]:
134
+ """A stand-in for `nuxi prepare`: tsconfig + auto-import / component declarations in a temp dir."""
135
+ d = Path(tempfile.mkdtemp(prefix="codegraph-nuxt-"))
136
+ atexit.register(shutil.rmtree, d, True)
137
+ src = (root / src_dir).resolve() if src_dir not in ("", ".") else root.resolve()
138
+ lines, n_imp = ["export {}", "declare global {"], 0
139
+ seen = set()
140
+ for sub in AUTO_IMPORT_DIRS:
141
+ base = src / sub
142
+ if not base.is_dir():
143
+ continue
144
+ files = [f for f in sorted(base.iterdir()) if f.is_file() and re.search(r"\.(ts|js|mjs|mts)$", f.name)
145
+ and not re.search(r"\.(test|spec|d)\.", f.name)]
146
+ files += [i for c in sorted(base.iterdir()) if c.is_dir() for i in (c / "index.ts", c / "index.js") if i.exists()]
147
+ for f in files:
148
+ spec = str(f.with_suffix("")).replace("\\", "/")
149
+ for local, exp in _exports(f):
150
+ if local in seen:
151
+ continue
152
+ seen.add(local)
153
+ acc = "['default']" if exp == "default" else f".{exp}"
154
+ lines.append(f" const {local}: typeof import('{spec}'){acc}")
155
+ n_imp += 1
156
+ nm = root / "node_modules"
157
+ builtins = 0
158
+ if (nm / "vue").is_dir():
159
+ for n in ("ref", "computed", "reactive", "watch", "watchEffect", "onMounted", "onBeforeUnmount", "onUnmounted",
160
+ "nextTick", "toRef", "toRefs", "unref", "shallowRef", "readonly"):
161
+ if n not in seen:
162
+ lines.append(f" const {n}: typeof import('vue')['{n}']")
163
+ builtins += 1
164
+ if (nm / "nuxt").is_dir():
165
+ for n in ("useRuntimeConfig", "useFetch", "useLazyFetch", "useAsyncData", "useLazyAsyncData", "useRoute", "useRouter",
166
+ "navigateTo", "useState", "useCookie", "useNuxtApp", "defineNuxtPlugin", "defineNuxtRouteMiddleware",
167
+ "definePageMeta", "useHead", "useRequestHeaders"):
168
+ if n not in seen:
169
+ lines.append(f" const {n}: typeof import('nuxt/app')['{n}']")
170
+ builtins += 1
171
+ if "$fetch" not in seen:
172
+ lines.append(" const $fetch: typeof import('ofetch')['$fetch']")
173
+ if (nm / "pinia").is_dir() and "defineStore" not in seen:
174
+ lines.append(" const defineStore: typeof import('pinia')['defineStore']")
175
+ lines.append("}")
176
+ (d / "types").mkdir()
177
+ (d / "types" / "imports.d.ts").write_text("\n".join(lines) + "\n")
178
+ (d / "nuxt.d.ts").write_text('/// <reference path="types/imports.d.ts" />\nexport {}\n')
179
+ comps = ["interface _GlobalComponents {"]
180
+ n_comp = 0
181
+ cbase = src / "components"
182
+ if cbase.is_dir():
183
+ for f in sorted(cbase.rglob("*")):
184
+ if f.is_file() and f.suffix in (".vue", ".tsx", ".jsx") and COMMON_SKIP.isdisjoint(f.parts):
185
+ name = _component_name(list(f.relative_to(cbase).parts))
186
+ comps.append(f' {name}: typeof import("{f}")[\'default\']')
187
+ comps.append(f' Lazy{name}: LazyComponent<typeof import("{f}")[\'default\']>')
188
+ n_comp += 1
189
+ comps.append("}")
190
+ (d / "types" / "components.d.ts").write_text("\n".join(comps) + "\n")
191
+ # the user's tsconfig (minus its reference to .nuxt) still contributes compilerOptions such as extra paths
192
+ user_opts = {}
193
+ try:
194
+ raw = re.sub(r"(?m)^\s*//.*$", "", (root / "tsconfig.json").read_text())
195
+ user_opts = (json.loads(re.sub(r",(\s*[}\]])", r"\1", raw)) or {}).get("compilerOptions") or {}
196
+ except Exception:
197
+ pass
198
+ paths = {k: [str((root / x).resolve()) for x in v] for k, v in (user_opts.get("paths") or {}).items()}
199
+ for alias, target in (("~", src), ("@", src), ("~~", root.resolve()), ("@@", root.resolve())):
200
+ paths.setdefault(f"{alias}/*", [f"{target}/*"])
201
+ paths.setdefault(alias, [str(target)])
202
+ cfg = {"compilerOptions": {"target": "ESNext", "module": "ESNext", "moduleResolution": "Bundler", "jsx": "preserve",
203
+ "allowJs": True, "checkJs": False, "strict": False, "skipLibCheck": True, "noEmit": True,
204
+ "resolveJsonModule": True, "esModuleInterop": True, "baseUrl": str(root.resolve()),
205
+ "paths": paths},
206
+ "include": [str(d / "nuxt.d.ts"), f"{src}/**/*"],
207
+ "exclude": [str(root.resolve() / x) for x in EXCLUDE] + [f"{src}/**/*.d.ts"]}
208
+ (d / "tsconfig.app.json").write_text(json.dumps(cfg, indent=1))
209
+ return d, {"auto_imports": n_imp, "builtins": builtins, "components": n_comp}
210
+
211
+
212
+ def flatten(d, prefix=""):
213
+ if isinstance(d, dict):
214
+ for k, v in d.items():
215
+ yield from flatten(v, f"{prefix}.{k}" if prefix else k)
216
+ else:
217
+ yield prefix
218
+
219
+
220
+ class NuxtPlugin(FrameworkPlugin):
221
+ name, language = "nuxt", "typescript"
222
+
223
+ def detect(self, project: Project) -> bool:
224
+ return any(project.exists(n) for n in ("nuxt.config.ts", "nuxt.config.js", "nuxt.config.mjs"))
225
+
226
+ def register_hooks(self, ctx) -> None:
227
+ root = ctx.project.root
228
+ cfg = ctx.extractor_cfg
229
+ self.src_dir = find_src_dir(root)
230
+ self.generated = {}
231
+ tsc = next((t for t in (".nuxt/tsconfig.app.json", ".nuxt/tsconfig.json") if (root / t).exists()), None)
232
+ self.prepared = "nuxi" if tsc else "generated"
233
+ if tsc:
234
+ cfg["tsconfig"] = tsc
235
+ cfg["components_dts"] = str(root / ".nuxt" / "types" / "components.d.ts")
236
+ if not Path(cfg["components_dts"]).exists() and (root / ".nuxt" / "components.d.ts").exists():
237
+ cfg["components_dts"] = str(root / ".nuxt" / "components.d.ts")
238
+ else:
239
+ d, self.generated = generate_types(root, self.src_dir)
240
+ cfg["tsconfig"] = str(d / "tsconfig.app.json")
241
+ cfg["components_dts"] = str(d / "types" / "components.d.ts")
242
+ cfg["walk_src"] = True
243
+ cfg["generated_types"] = True
244
+ print(f"nuxt: no .nuxt/ in {root.name} (run `npx nuxi prepare` for Nuxt's own generated types); using "
245
+ f"generated stand-ins: {self.generated['auto_imports']} auto-imports, {self.generated['components']} "
246
+ f"components{'' if (root / 'node_modules').is_dir() else ', no node_modules so Vue/Nuxt built-ins stay unresolved'}",
247
+ file=sys.stderr)
248
+ cfg["kinds"] = kind_rules(self.src_dir)
249
+ cfg["src_dirs"] = [self.src_dir]
250
+
251
+ def contribute(self, project: Project, builder: GraphBuilder, ctx) -> dict:
252
+ st = {"prepared": self.prepared, "src_dir": self.src_dir, "pages": 0, "layouts": 0, "ui_global": 0,
253
+ "i18n_keys_used": 0, "i18n_keys_defined": 0}
254
+ if self.generated:
255
+ st["generated_types"] = self.generated
256
+ if not ctx or not ctx.facts:
257
+ return {**st, "status": "no facts"}
258
+ layouts = {}
259
+ for n in builder.nodes.values():
260
+ if n.lang != "ts" or not n.file:
261
+ continue
262
+ if n.kind == "page":
263
+ route = nuxt_route(n.file, self.src_dir)
264
+ n.attrs["route"] = route
265
+ n.name = route
266
+ n.fqn = route
267
+ n.entry_kind = "ui_page"
268
+ st["pages"] += 1
269
+ elif n.kind == "layout":
270
+ layouts[Path(n.file).stem] = n.id
271
+ n.entry_kind = "ui_global"
272
+ st["layouts"] += 1
273
+ elif n.kind == "app" or (n.kind == "module" and (n.attrs.get("file_kind") == "plugin" or
274
+ (n.attrs.get("file_kind") == "middleware" and ".global." in n.file))):
275
+ n.entry_kind = "ui_global"
276
+ st["ui_global"] += 1
277
+ meta = ctx.facts.get("page_meta") or {}
278
+ for n in list(builder.nodes.values()):
279
+ if n.kind != "page":
280
+ continue
281
+ lay = (meta.get(n.file) or {}).get("layout") or "default"
282
+ if lay in layouts:
283
+ builder.add_edge(n.id, layouts[lay], "USES_LAYOUT", file=n.file, line=1,
284
+ confidence="exact" if n.file in meta and meta[n.file].get("layout") else "resolved")
285
+ # i18n: keys used in code/templates, and where they are defined
286
+ defined = {}
287
+ for d in dict.fromkeys(("i18n/locales", f"{self.src_dir}/locales", "app/locales", "locales")):
288
+ for f in sorted((project.root / d).glob("*.json")) if (project.root / d).exists() else []:
289
+ try:
290
+ data = json.loads(f.read_text())
291
+ except Exception:
292
+ continue
293
+ locs = {"en", "ar", "ku", "fr", "de"}
294
+ if isinstance(data, dict) and set(data) and set(data) <= locs:
295
+ data = next(iter(data.values()))
296
+ for k in flatten(data):
297
+ defined.setdefault(k, []).append(str(f.relative_to(project.root)))
298
+ for f, keys in (ctx.facts.get("sfc_i18n") or {}).items():
299
+ for k in keys:
300
+ defined.setdefault(k, []).append(f)
301
+ for u in ctx.facts.get("i18n") or []:
302
+ nid = builder.add_node("i18n", u["key"], u["key"], lang="ts", attrs={"defined_in": defined.get(u["key"], [])})
303
+ builder.add_edge(u["src"], nid, "USES_I18N", file=u["file"], line=u["line"],
304
+ confidence="exact" if u["key"] in defined else "heuristic")
305
+ st["i18n_keys_used"] = len({u["key"] for u in ctx.facts.get("i18n") or []})
306
+ st["i18n_keys_defined"] = len(defined)
307
+ st["i18n_used_but_undefined"] = len({u["key"] for u in ctx.facts.get("i18n") or []} - set(defined))
308
+ return st
File without changes
@@ -0,0 +1,5 @@
1
+ {
2
+ "require": {
3
+ "nikic/php-parser": "^5.4"
4
+ }
5
+ }