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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- 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
|