@mgamil/mapx 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +194 -0
- package/README.md +488 -0
- package/VERSION +1 -0
- package/dist/agents/generator.d.ts +74 -0
- package/dist/agents/generator.js +375 -0
- package/dist/agents/templates.d.ts +29 -0
- package/dist/agents/templates.js +459 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +1835 -0
- package/dist/core/cluster-engine.d.ts +32 -0
- package/dist/core/cluster-engine.js +314 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.js +178 -0
- package/dist/core/context-builder.d.ts +61 -0
- package/dist/core/context-builder.js +252 -0
- package/dist/core/flow-tracer.d.ts +63 -0
- package/dist/core/flow-tracer.js +366 -0
- package/dist/core/git-tracker.d.ts +20 -0
- package/dist/core/git-tracker.js +159 -0
- package/dist/core/graph.d.ts +42 -0
- package/dist/core/graph.js +186 -0
- package/dist/core/metrics.d.ts +24 -0
- package/dist/core/metrics.js +87 -0
- package/dist/core/scanner.d.ts +53 -0
- package/dist/core/scanner.js +949 -0
- package/dist/core/store-bun.d.ts +13 -0
- package/dist/core/store-bun.js +34 -0
- package/dist/core/store-interface.d.ts +15 -0
- package/dist/core/store-interface.js +7 -0
- package/dist/core/store-node.d.ts +13 -0
- package/dist/core/store-node.js +35 -0
- package/dist/core/store.d.ts +132 -0
- package/dist/core/store.js +614 -0
- package/dist/core/workspace-manager.d.ts +9 -0
- package/dist/core/workspace-manager.js +64 -0
- package/dist/exporters/dot-exporter.d.ts +16 -0
- package/dist/exporters/dot-exporter.js +179 -0
- package/dist/exporters/graph-exporter.d.ts +14 -0
- package/dist/exporters/graph-exporter.js +85 -0
- package/dist/exporters/index.d.ts +9 -0
- package/dist/exporters/index.js +12 -0
- package/dist/exporters/llm-exporter.d.ts +18 -0
- package/dist/exporters/llm-exporter.js +224 -0
- package/dist/exporters/svg-exporter.d.ts +19 -0
- package/dist/exporters/svg-exporter.js +319 -0
- package/dist/exporters/toon-exporter.d.ts +16 -0
- package/dist/exporters/toon-exporter.js +246 -0
- package/dist/frameworks/detectors/aspnet.d.ts +11 -0
- package/dist/frameworks/detectors/aspnet.js +52 -0
- package/dist/frameworks/detectors/django.d.ts +14 -0
- package/dist/frameworks/detectors/django.js +135 -0
- package/dist/frameworks/detectors/drupal.d.ts +13 -0
- package/dist/frameworks/detectors/drupal.js +94 -0
- package/dist/frameworks/detectors/express.d.ts +12 -0
- package/dist/frameworks/detectors/express.js +234 -0
- package/dist/frameworks/detectors/fastapi.d.ts +12 -0
- package/dist/frameworks/detectors/fastapi.js +203 -0
- package/dist/frameworks/detectors/flask.d.ts +12 -0
- package/dist/frameworks/detectors/flask.js +244 -0
- package/dist/frameworks/detectors/go.d.ts +11 -0
- package/dist/frameworks/detectors/go.js +75 -0
- package/dist/frameworks/detectors/laravel.d.ts +11 -0
- package/dist/frameworks/detectors/laravel.js +462 -0
- package/dist/frameworks/detectors/nestjs.d.ts +12 -0
- package/dist/frameworks/detectors/nestjs.js +155 -0
- package/dist/frameworks/detectors/nextjs.d.ts +11 -0
- package/dist/frameworks/detectors/nextjs.js +118 -0
- package/dist/frameworks/detectors/rails.d.ts +12 -0
- package/dist/frameworks/detectors/rails.js +76 -0
- package/dist/frameworks/detectors/react-router.d.ts +11 -0
- package/dist/frameworks/detectors/react-router.js +115 -0
- package/dist/frameworks/detectors/rust.d.ts +11 -0
- package/dist/frameworks/detectors/rust.js +59 -0
- package/dist/frameworks/detectors/spring.d.ts +11 -0
- package/dist/frameworks/detectors/spring.js +56 -0
- package/dist/frameworks/detectors/sveltekit.d.ts +11 -0
- package/dist/frameworks/detectors/sveltekit.js +154 -0
- package/dist/frameworks/detectors/symfony.d.ts +13 -0
- package/dist/frameworks/detectors/symfony.js +175 -0
- package/dist/frameworks/detectors/tanstack-router.d.ts +12 -0
- package/dist/frameworks/detectors/tanstack-router.js +80 -0
- package/dist/frameworks/detectors/vapor.d.ts +11 -0
- package/dist/frameworks/detectors/vapor.js +52 -0
- package/dist/frameworks/detectors/vue-router.d.ts +12 -0
- package/dist/frameworks/detectors/vue-router.js +237 -0
- package/dist/frameworks/detectors/wordpress.d.ts +13 -0
- package/dist/frameworks/detectors/wordpress.js +141 -0
- package/dist/frameworks/detectors/yii.d.ts +11 -0
- package/dist/frameworks/detectors/yii.js +131 -0
- package/dist/frameworks/framework-registry.d.ts +13 -0
- package/dist/frameworks/framework-registry.js +77 -0
- package/dist/frameworks/route-registry.d.ts +26 -0
- package/dist/frameworks/route-registry.js +102 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +30 -0
- package/dist/languages/index.d.ts +2 -0
- package/dist/languages/index.js +7 -0
- package/dist/languages/installer.d.ts +13 -0
- package/dist/languages/installer.js +103 -0
- package/dist/languages/registry.d.ts +19 -0
- package/dist/languages/registry.js +427 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +20 -0
- package/dist/mcp.d.ts +11 -0
- package/dist/mcp.js +1699 -0
- package/dist/parsers/common-methods.d.ts +3 -0
- package/dist/parsers/common-methods.js +33 -0
- package/dist/parsers/fallback-parser.d.ts +10 -0
- package/dist/parsers/fallback-parser.js +18 -0
- package/dist/parsers/generic-wasm-parser.d.ts +23 -0
- package/dist/parsers/generic-wasm-parser.js +168 -0
- package/dist/parsers/ignored-symbols.d.ts +26 -0
- package/dist/parsers/ignored-symbols.js +77 -0
- package/dist/parsers/index.d.ts +9 -0
- package/dist/parsers/index.js +13 -0
- package/dist/parsers/languages/javascript.d.ts +11 -0
- package/dist/parsers/languages/javascript.js +28 -0
- package/dist/parsers/languages/php.d.ts +15 -0
- package/dist/parsers/languages/php.js +648 -0
- package/dist/parsers/languages/typescript.d.ts +10 -0
- package/dist/parsers/languages/typescript.js +9 -0
- package/dist/parsers/languages/vue.d.ts +13 -0
- package/dist/parsers/languages/vue.js +63 -0
- package/dist/parsers/parse-worker.d.ts +2 -0
- package/dist/parsers/parse-worker.js +185 -0
- package/dist/parsers/parser-interface.d.ts +9 -0
- package/dist/parsers/parser-interface.js +0 -0
- package/dist/parsers/parser-registry.d.ts +8 -0
- package/dist/parsers/parser-registry.js +52 -0
- package/dist/parsers/wasm-parser.d.ts +16 -0
- package/dist/parsers/wasm-parser.js +110 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.js +0 -0
- package/dist/ui/index.html +270 -0
- package/dist/ui/main.js +581 -0
- package/dist/ui/main.js.map +7 -0
- package/dist/ui/styles.css +573 -0
- package/dist/ui-events.d.ts +36 -0
- package/dist/ui-events.js +61 -0
- package/dist/ui-server.d.ts +12 -0
- package/dist/ui-server.js +504 -0
- package/package.json +179 -0
- package/queries/bash/references.scm +22 -0
- package/queries/bash/symbols.scm +15 -0
- package/queries/c/references.scm +14 -0
- package/queries/c/symbols.scm +30 -0
- package/queries/c-sharp/references.scm +26 -0
- package/queries/c-sharp/symbols.scm +57 -0
- package/queries/cpp/references.scm +21 -0
- package/queries/cpp/symbols.scm +44 -0
- package/queries/dart/references.scm +33 -0
- package/queries/dart/symbols.scm +38 -0
- package/queries/elixir/references.scm +45 -0
- package/queries/elixir/symbols.scm +41 -0
- package/queries/go/references.scm +22 -0
- package/queries/go/symbols.scm +53 -0
- package/queries/java/references.scm +32 -0
- package/queries/java/symbols.scm +41 -0
- package/queries/javascript/references.scm +14 -0
- package/queries/javascript/symbols.scm +23 -0
- package/queries/kotlin/references.scm +31 -0
- package/queries/kotlin/symbols.scm +24 -0
- package/queries/lua/references.scm +19 -0
- package/queries/lua/symbols.scm +29 -0
- package/queries/pascal/references.scm +29 -0
- package/queries/pascal/symbols.scm +45 -0
- package/queries/php/references.scm +109 -0
- package/queries/php/symbols.scm +33 -0
- package/queries/python/references.scm +50 -0
- package/queries/python/symbols.scm +21 -0
- package/queries/ruby/references.scm +48 -0
- package/queries/ruby/symbols.scm +24 -0
- package/queries/rust/references.scm +31 -0
- package/queries/rust/symbols.scm +35 -0
- package/queries/scala/references.scm +30 -0
- package/queries/scala/symbols.scm +35 -0
- package/queries/svelte/references.scm +20 -0
- package/queries/svelte/symbols.scm +30 -0
- package/queries/swift/references.scm +22 -0
- package/queries/swift/symbols.scm +37 -0
- package/queries/typescript/references.scm +25 -0
- package/queries/typescript/symbols.scm +35 -0
- package/queries/vue/references.scm +20 -0
- package/queries/vue/symbols.scm +28 -0
- package/queries/zig/references.scm +20 -0
- package/queries/zig/symbols.scm +22 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { DotExporter } from "./dot-exporter.js";
|
|
3
|
+
class SvgExporter {
|
|
4
|
+
store;
|
|
5
|
+
graph;
|
|
6
|
+
constructor(store, graph) {
|
|
7
|
+
this.store = store;
|
|
8
|
+
this.graph = graph;
|
|
9
|
+
}
|
|
10
|
+
export(repo, filesFilter, opts) {
|
|
11
|
+
if (opts?.forceFallback) {
|
|
12
|
+
return this.renderFallback(repo, filesFilter, opts);
|
|
13
|
+
}
|
|
14
|
+
const dotExporter = new DotExporter(this.store, this.graph);
|
|
15
|
+
const dot = dotExporter.export(repo, filesFilter, opts);
|
|
16
|
+
try {
|
|
17
|
+
return execSync("dot -Tsvg", {
|
|
18
|
+
input: dot,
|
|
19
|
+
encoding: "utf-8",
|
|
20
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
21
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
22
|
+
});
|
|
23
|
+
} catch {
|
|
24
|
+
return this.renderFallback(repo, filesFilter, opts);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
renderFallback(repo, filesFilter, opts) {
|
|
28
|
+
const clusterMode = opts?.cluster ?? "none";
|
|
29
|
+
let files = this.store.getAllFiles(repo);
|
|
30
|
+
let edges = this.store.getAllEdges(repo);
|
|
31
|
+
let rankedFiles = this.graph.getRankedFiles();
|
|
32
|
+
if (filesFilter) {
|
|
33
|
+
const allowed = new Set(filesFilter);
|
|
34
|
+
files = files.filter((f) => allowed.has(f.path));
|
|
35
|
+
edges = edges.filter((e) => allowed.has(e.source_file) && allowed.has(e.target_file));
|
|
36
|
+
rankedFiles = rankedFiles.filter((f) => allowed.has(f.path));
|
|
37
|
+
}
|
|
38
|
+
const langColors = {
|
|
39
|
+
php: "#4f5b93",
|
|
40
|
+
javascript: "#eab308",
|
|
41
|
+
typescript: "#2563eb",
|
|
42
|
+
python: "#3b82f6",
|
|
43
|
+
go: "#06b6d4",
|
|
44
|
+
rust: "#f97316",
|
|
45
|
+
java: "#ea580c",
|
|
46
|
+
c: "#0284c7",
|
|
47
|
+
cpp: "#0284c7",
|
|
48
|
+
csharp: "#0891b2",
|
|
49
|
+
ruby: "#dc2626",
|
|
50
|
+
swift: "#f97316",
|
|
51
|
+
kotlin: "#7c3aed",
|
|
52
|
+
scala: "#dc2626",
|
|
53
|
+
shell: "#10b981",
|
|
54
|
+
html: "#f97316",
|
|
55
|
+
css: "#2563eb",
|
|
56
|
+
sql: "#005b96",
|
|
57
|
+
yaml: "#78716c",
|
|
58
|
+
json: "#78716c",
|
|
59
|
+
markdown: "#0f172a"
|
|
60
|
+
};
|
|
61
|
+
const maxPr = Math.max(...rankedFiles.map((f) => f.pagerank), 1e-3);
|
|
62
|
+
const rankMap = new Map(rankedFiles.map((f) => [f.path, f.pagerank]));
|
|
63
|
+
const TARGET_WIDTH = 1200;
|
|
64
|
+
const MARGIN = 40;
|
|
65
|
+
const CARD_PADDING = 16;
|
|
66
|
+
const TITLE_HEIGHT = 28;
|
|
67
|
+
const CARD_GAP = 24;
|
|
68
|
+
const ITEM_W = 160;
|
|
69
|
+
const ITEM_H = 32;
|
|
70
|
+
const ITEM_GAP_X = 12;
|
|
71
|
+
const ITEM_GAP_Y = 8;
|
|
72
|
+
const nodePositions = /* @__PURE__ */ new Map();
|
|
73
|
+
let svgW = TARGET_WIDTH;
|
|
74
|
+
let svgH = 0;
|
|
75
|
+
const cardLines = [];
|
|
76
|
+
if (clusterMode !== "none") {
|
|
77
|
+
const clusters = this.store.getClusters(repo);
|
|
78
|
+
const memberships = this.store.getClusterMemberships(repo);
|
|
79
|
+
const primaryMemberships = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const m of memberships) {
|
|
81
|
+
if (m.is_primary === 1) {
|
|
82
|
+
primaryMemberships.set(m.file_path, m.cluster_name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const clusterMap = /* @__PURE__ */ new Map();
|
|
86
|
+
const unclustered = [];
|
|
87
|
+
for (const f of files) {
|
|
88
|
+
const cName = primaryMemberships.get(f.path);
|
|
89
|
+
if (cName) {
|
|
90
|
+
if (!clusterMap.has(cName)) {
|
|
91
|
+
clusterMap.set(cName, []);
|
|
92
|
+
}
|
|
93
|
+
clusterMap.get(cName).push(f);
|
|
94
|
+
} else {
|
|
95
|
+
unclustered.push(f);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const getCols = (N) => {
|
|
99
|
+
if (N <= 3) return 1;
|
|
100
|
+
if (N <= 8) return 2;
|
|
101
|
+
if (N <= 15) return 3;
|
|
102
|
+
if (N <= 30) return 4;
|
|
103
|
+
return 5;
|
|
104
|
+
};
|
|
105
|
+
const groups = [];
|
|
106
|
+
for (const [cName, fList] of clusterMap.entries()) {
|
|
107
|
+
const clusterInfo = clusters.find((c) => c.name === cName);
|
|
108
|
+
const label = String(clusterInfo?.label || cName);
|
|
109
|
+
fList.sort((a, b) => {
|
|
110
|
+
const prA = rankMap.get(a.path) || 0;
|
|
111
|
+
const prB = rankMap.get(b.path) || 0;
|
|
112
|
+
return prB - prA;
|
|
113
|
+
});
|
|
114
|
+
const N = fList.length;
|
|
115
|
+
const cols = getCols(N);
|
|
116
|
+
const width = CARD_PADDING * 2 + cols * ITEM_W + (cols - 1) * ITEM_GAP_X;
|
|
117
|
+
const rows = Math.ceil(N / cols);
|
|
118
|
+
const height = CARD_PADDING * 2 + TITLE_HEIGHT + rows * ITEM_H + (rows - 1) * ITEM_GAP_Y;
|
|
119
|
+
groups.push({
|
|
120
|
+
name: cName,
|
|
121
|
+
label,
|
|
122
|
+
files: fList,
|
|
123
|
+
cols,
|
|
124
|
+
width,
|
|
125
|
+
height
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (unclustered.length > 0) {
|
|
129
|
+
unclustered.sort((a, b) => {
|
|
130
|
+
const prA = rankMap.get(a.path) || 0;
|
|
131
|
+
const prB = rankMap.get(b.path) || 0;
|
|
132
|
+
return prB - prA;
|
|
133
|
+
});
|
|
134
|
+
const N = unclustered.length;
|
|
135
|
+
const cols = getCols(N);
|
|
136
|
+
const width = CARD_PADDING * 2 + cols * ITEM_W + (cols - 1) * ITEM_GAP_X;
|
|
137
|
+
const rows = Math.ceil(N / cols);
|
|
138
|
+
const height = CARD_PADDING * 2 + TITLE_HEIGHT + rows * ITEM_H + (rows - 1) * ITEM_GAP_Y;
|
|
139
|
+
groups.push({
|
|
140
|
+
name: "__unclustered__",
|
|
141
|
+
label: "Other Files",
|
|
142
|
+
files: unclustered,
|
|
143
|
+
cols,
|
|
144
|
+
width,
|
|
145
|
+
height
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
groups.sort((a, b) => b.files.length - a.files.length);
|
|
149
|
+
let currentX = MARGIN;
|
|
150
|
+
let currentY = MARGIN;
|
|
151
|
+
let rowMaxHeight = 0;
|
|
152
|
+
for (const group of groups) {
|
|
153
|
+
if (currentX + group.width > TARGET_WIDTH - MARGIN && currentX > MARGIN) {
|
|
154
|
+
currentX = MARGIN;
|
|
155
|
+
currentY += rowMaxHeight + CARD_GAP;
|
|
156
|
+
rowMaxHeight = 0;
|
|
157
|
+
}
|
|
158
|
+
group.x = currentX;
|
|
159
|
+
group.y = currentY;
|
|
160
|
+
cardLines.push(
|
|
161
|
+
` <!-- Cluster Group: ${this.escXml(group.name)} -->`,
|
|
162
|
+
` <rect x="${group.x}" y="${group.y}" width="${group.width}" height="${group.height}" rx="12" fill="#131b2e" stroke="#1e293b" stroke-width="1.5" class="cluster-card"/>`,
|
|
163
|
+
` <text x="${group.x + CARD_PADDING}" y="${group.y + CARD_PADDING + 14}" fill="#94a3b8" font-family="system-ui, -apple-system, sans-serif" font-size="12" font-weight="600" letter-spacing="0.5">${this.escXml(group.label.toUpperCase())}</text>`
|
|
164
|
+
);
|
|
165
|
+
const gX = group.x;
|
|
166
|
+
const gY = group.y;
|
|
167
|
+
const cols = group.cols;
|
|
168
|
+
for (let idx = 0; idx < group.files.length; idx++) {
|
|
169
|
+
const file = group.files[idx];
|
|
170
|
+
const col = idx % cols;
|
|
171
|
+
const row = Math.floor(idx / cols);
|
|
172
|
+
const nodeX = gX + CARD_PADDING + col * (ITEM_W + ITEM_GAP_X);
|
|
173
|
+
const nodeY = gY + CARD_PADDING + TITLE_HEIGHT + row * (ITEM_H + ITEM_GAP_Y);
|
|
174
|
+
nodePositions.set(file.path, {
|
|
175
|
+
x: nodeX,
|
|
176
|
+
y: nodeY,
|
|
177
|
+
w: ITEM_W,
|
|
178
|
+
h: ITEM_H,
|
|
179
|
+
file
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
currentX += group.width + CARD_GAP;
|
|
183
|
+
if (group.height > rowMaxHeight) {
|
|
184
|
+
rowMaxHeight = group.height;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
svgH = currentY + rowMaxHeight + MARGIN;
|
|
188
|
+
} else {
|
|
189
|
+
const GAP_X = 16;
|
|
190
|
+
const GAP_Y = 12;
|
|
191
|
+
const cols = Math.max(1, Math.floor((TARGET_WIDTH - MARGIN * 2 + GAP_X) / (ITEM_W + GAP_X)));
|
|
192
|
+
files.sort((a, b) => {
|
|
193
|
+
const prA = rankMap.get(a.path) || 0;
|
|
194
|
+
const prB = rankMap.get(b.path) || 0;
|
|
195
|
+
return prB - prA;
|
|
196
|
+
});
|
|
197
|
+
for (let i = 0; i < files.length; i++) {
|
|
198
|
+
const col = i % cols;
|
|
199
|
+
const row = Math.floor(i / cols);
|
|
200
|
+
const nodeX = MARGIN + col * (ITEM_W + GAP_X);
|
|
201
|
+
const nodeY = MARGIN + row * (ITEM_H + GAP_Y);
|
|
202
|
+
nodePositions.set(files[i].path, {
|
|
203
|
+
x: nodeX,
|
|
204
|
+
y: nodeY,
|
|
205
|
+
w: ITEM_W,
|
|
206
|
+
h: ITEM_H,
|
|
207
|
+
file: files[i]
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const rows = Math.ceil(files.length / cols);
|
|
211
|
+
svgH = MARGIN * 2 + rows * ITEM_H + (rows - 1) * GAP_Y;
|
|
212
|
+
}
|
|
213
|
+
const edgeStyles = {
|
|
214
|
+
import: { stroke: "#64748b", dash: "" },
|
|
215
|
+
require: { stroke: "#64748b", dash: "" },
|
|
216
|
+
extends: { stroke: "#3b82f6", dash: "" },
|
|
217
|
+
implements: { stroke: "#10b981", dash: "4,4" },
|
|
218
|
+
call: { stroke: "#f59e0b", dash: "3,3" },
|
|
219
|
+
instantiation: { stroke: "#8b5cf6", dash: "3,3" },
|
|
220
|
+
relation: { stroke: "#3b82f6", dash: "" },
|
|
221
|
+
route: { stroke: "#10b981", dash: "" },
|
|
222
|
+
binding: { stroke: "#8b5cf6", dash: "4,4" },
|
|
223
|
+
middleware: { stroke: "#f97316", dash: "2,2" }
|
|
224
|
+
};
|
|
225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
226
|
+
const edgeLines = [];
|
|
227
|
+
for (const edge of edges) {
|
|
228
|
+
const src = edge.source_file;
|
|
229
|
+
const tgt = edge.target_file;
|
|
230
|
+
const type = edge.edge_type;
|
|
231
|
+
const key = `${src}->${tgt}:${type}`;
|
|
232
|
+
if (seen.has(key)) continue;
|
|
233
|
+
seen.add(key);
|
|
234
|
+
const srcNode = nodePositions.get(src);
|
|
235
|
+
const tgtNode = nodePositions.get(tgt);
|
|
236
|
+
if (!srcNode || !tgtNode) continue;
|
|
237
|
+
const sx = srcNode.x + srcNode.w / 2;
|
|
238
|
+
const sy = srcNode.y + ITEM_H;
|
|
239
|
+
const tx = tgtNode.x + tgtNode.w / 2;
|
|
240
|
+
const ty = tgtNode.y;
|
|
241
|
+
const style = edgeStyles[type] || { stroke: "#64748b", dash: "" };
|
|
242
|
+
const dash = edge.verifiability === "inferred" ? "5,5" : style.dash;
|
|
243
|
+
const dashAttr = dash ? ` stroke-dasharray="${dash}"` : "";
|
|
244
|
+
const midY = (sy + ty) / 2;
|
|
245
|
+
edgeLines.push(
|
|
246
|
+
` <path d="M${sx},${sy} C${sx},${midY} ${tx},${midY} ${tx},${ty}" fill="none" stroke="${style.stroke}" stroke-width="1.2"${dashAttr} opacity="0.35" class="edge-path" marker-end="url(#arrow)"/>`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const nodeLines = [];
|
|
250
|
+
for (const [path, n] of nodePositions.entries()) {
|
|
251
|
+
const pr = rankMap.get(path) || 0;
|
|
252
|
+
const opacity = 0.7 + 0.3 * (pr / maxPr);
|
|
253
|
+
const textY = n.y + ITEM_H / 2 + 4.5;
|
|
254
|
+
const color = langColors[n.file.language] || "#CCCCCC";
|
|
255
|
+
const textColor = "#ffffff";
|
|
256
|
+
const isHighRank = pr > maxPr * 0.5;
|
|
257
|
+
const strokeAttr = isHighRank ? ` stroke="#60a5fa" stroke-width="1.5"` : ` stroke="#1e293b" stroke-width="1"`;
|
|
258
|
+
const filePath = String(n.file.path || "");
|
|
259
|
+
nodeLines.push(
|
|
260
|
+
` <g class="node-group">`,
|
|
261
|
+
` <rect x="${n.x}" y="${n.y}" width="${n.w}" height="${ITEM_H}" rx="6" fill="${color}" opacity="${opacity.toFixed(2)}"${strokeAttr} class="node-rect"/>`,
|
|
262
|
+
` <text x="${n.x + n.w / 2}" y="${textY}" text-anchor="middle" fill="${textColor}" font-family="system-ui, -apple-system, sans-serif" font-size="11" font-weight="500">${this.escXml(filePath.split("/").pop() || filePath)}</text>`,
|
|
263
|
+
` <title>${this.escXml(filePath)} (PageRank: ${pr.toFixed(4)})</title>`,
|
|
264
|
+
` </g>`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const styleBlock = [
|
|
268
|
+
" <style>",
|
|
269
|
+
" .cluster-card {",
|
|
270
|
+
" transition: stroke 0.2s ease, fill 0.2s ease;",
|
|
271
|
+
" }",
|
|
272
|
+
" .cluster-card:hover {",
|
|
273
|
+
" stroke: #475569;",
|
|
274
|
+
" fill: #1e293b;",
|
|
275
|
+
" }",
|
|
276
|
+
" .node-rect {",
|
|
277
|
+
" transition: filter 0.2s ease, stroke 0.2s ease;",
|
|
278
|
+
" }",
|
|
279
|
+
" .node-rect:hover {",
|
|
280
|
+
" filter: brightness(1.2);",
|
|
281
|
+
" stroke: #60a5fa;",
|
|
282
|
+
" stroke-width: 1.5px;",
|
|
283
|
+
" cursor: pointer;",
|
|
284
|
+
" }",
|
|
285
|
+
" .edge-path {",
|
|
286
|
+
" transition: opacity 0.2s ease, stroke-width 0.2s ease;",
|
|
287
|
+
" }",
|
|
288
|
+
" .edge-path:hover {",
|
|
289
|
+
" opacity: 0.85;",
|
|
290
|
+
" stroke-width: 2px;",
|
|
291
|
+
" }",
|
|
292
|
+
" </style>"
|
|
293
|
+
].join("\n");
|
|
294
|
+
const defs = [
|
|
295
|
+
" <defs>",
|
|
296
|
+
' <marker id="arrow" viewBox="0 0 10 10" refX="6" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">',
|
|
297
|
+
' <path d="M 0 2 L 8 5 L 0 8 z" fill="#64748b"/>',
|
|
298
|
+
" </marker>",
|
|
299
|
+
" </defs>"
|
|
300
|
+
].join("\n");
|
|
301
|
+
return [
|
|
302
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
303
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgW} ${svgH}" width="${svgW}" height="${svgH}">`,
|
|
304
|
+
styleBlock,
|
|
305
|
+
defs,
|
|
306
|
+
' <rect width="100%" height="100%" fill="#0b0f19"/>',
|
|
307
|
+
...cardLines,
|
|
308
|
+
...edgeLines,
|
|
309
|
+
...nodeLines,
|
|
310
|
+
"</svg>"
|
|
311
|
+
].join("\n");
|
|
312
|
+
}
|
|
313
|
+
escXml(s) {
|
|
314
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
export {
|
|
318
|
+
SvgExporter
|
|
319
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Store } from '../core/store.js';
|
|
2
|
+
import { MapxGraph } from '../core/graph.js';
|
|
3
|
+
import { ExportOptions } from '../types.js';
|
|
4
|
+
import '../core/store-interface.js';
|
|
5
|
+
|
|
6
|
+
declare function toonQuote(value: string, activeDelimiter?: ',' | '\t' | '|'): string;
|
|
7
|
+
declare function formatNumber(n: number): string;
|
|
8
|
+
declare class ToonExporter {
|
|
9
|
+
private store;
|
|
10
|
+
private graph;
|
|
11
|
+
constructor(store: Store, graph: MapxGraph);
|
|
12
|
+
export(options?: Partial<ExportOptions>): string;
|
|
13
|
+
private generateDocument;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { ToonExporter, formatNumber, toonQuote };
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
function toonQuote(value, activeDelimiter = ",") {
|
|
2
|
+
const needsQuoting = value === "" || /^\s|\s$/.test(value) || value === "true" || value === "false" || value === "null" || /^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value) || /[:"\\\[\]{}\u0000-\u001F\u007F-\u009F]/.test(value) || value === "-" || /^-\S/.test(value) || value.includes(activeDelimiter);
|
|
3
|
+
if (!needsQuoting) return value;
|
|
4
|
+
return '"' + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t").replace(/[\u0000-\u001F\u007F-\u009F]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`) + '"';
|
|
5
|
+
}
|
|
6
|
+
function formatNumber(n) {
|
|
7
|
+
if (Object.is(n, -0)) return "0";
|
|
8
|
+
if (isNaN(n) || !isFinite(n)) return "null";
|
|
9
|
+
if (n === 0) return "0";
|
|
10
|
+
const abs = Math.abs(n);
|
|
11
|
+
if (abs >= 1e-6 && abs < 1e21) {
|
|
12
|
+
let s = n.toString();
|
|
13
|
+
if (s.includes("e") || s.includes("E")) {
|
|
14
|
+
s = n.toFixed(20).replace(/\.?0+$/, "");
|
|
15
|
+
}
|
|
16
|
+
return s;
|
|
17
|
+
} else {
|
|
18
|
+
return n.toString().toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
class ToonExporter {
|
|
22
|
+
store;
|
|
23
|
+
graph;
|
|
24
|
+
constructor(store, graph) {
|
|
25
|
+
this.store = store;
|
|
26
|
+
this.graph = graph;
|
|
27
|
+
}
|
|
28
|
+
export(options) {
|
|
29
|
+
const opt = options || {};
|
|
30
|
+
const budget = opt.tokenBudget || 8192;
|
|
31
|
+
const delimiterName = opt.delimiter || "comma";
|
|
32
|
+
const delimiterMap = { comma: ",", tab: " ", pipe: "|" };
|
|
33
|
+
const delim = delimiterMap[delimiterName];
|
|
34
|
+
let files = this.store.getAllFiles(opt.repo);
|
|
35
|
+
let symbols = this.store.getAllSymbols(opt.repo);
|
|
36
|
+
let edges = this.store.getAllEdges(opt.repo);
|
|
37
|
+
const clusters = this.store.getClusters(opt.repo);
|
|
38
|
+
let rankedFiles = this.graph.getRankedFiles();
|
|
39
|
+
let rankedSymbols = this.graph.getRankedSymbols();
|
|
40
|
+
if (opt.files) {
|
|
41
|
+
const allowed = new Set(opt.files);
|
|
42
|
+
files = files.filter((f) => allowed.has(f.path));
|
|
43
|
+
symbols = symbols.filter((s) => allowed.has(s.file_path));
|
|
44
|
+
edges = edges.filter((e) => allowed.has(e.source_file) && allowed.has(e.target_file));
|
|
45
|
+
rankedFiles = rankedFiles.filter((f) => allowed.has(f.path));
|
|
46
|
+
rankedSymbols = rankedSymbols.filter((s) => allowed.has(s.filePath));
|
|
47
|
+
}
|
|
48
|
+
let low = 0;
|
|
49
|
+
let high = symbols.length;
|
|
50
|
+
let bestOutput = "";
|
|
51
|
+
const sortedSymbols = [...symbols].sort((a, b) => {
|
|
52
|
+
const rankA = rankedSymbols.find((rs) => rs.name === a.name && rs.filePath === a.file_path)?.pagerank || 0;
|
|
53
|
+
const rankB = rankedSymbols.find((rs) => rs.name === b.name && rs.filePath === b.file_path)?.pagerank || 0;
|
|
54
|
+
return rankB - rankA;
|
|
55
|
+
});
|
|
56
|
+
while (low <= high) {
|
|
57
|
+
const mid = Math.floor((low + high) / 2);
|
|
58
|
+
const output = this.generateDocument({
|
|
59
|
+
opt,
|
|
60
|
+
delim,
|
|
61
|
+
files,
|
|
62
|
+
rankedFiles,
|
|
63
|
+
symbols: sortedSymbols.slice(0, mid),
|
|
64
|
+
rankedSymbols,
|
|
65
|
+
edges,
|
|
66
|
+
clusters,
|
|
67
|
+
isTruncated: mid < symbols.length,
|
|
68
|
+
truncatedCount: symbols.length - mid,
|
|
69
|
+
originalSymbolsCount: symbols.length
|
|
70
|
+
});
|
|
71
|
+
const estimatedTokens = Math.ceil(Buffer.byteLength(output, "utf8") / 4);
|
|
72
|
+
if (estimatedTokens <= budget) {
|
|
73
|
+
bestOutput = output;
|
|
74
|
+
low = mid + 1;
|
|
75
|
+
} else {
|
|
76
|
+
high = mid - 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!bestOutput || Math.ceil(Buffer.byteLength(bestOutput, "utf8") / 4) > budget) {
|
|
80
|
+
let lowEdge = 0;
|
|
81
|
+
let highEdge = edges.length;
|
|
82
|
+
let bestEdgeOutput = "";
|
|
83
|
+
while (lowEdge <= highEdge) {
|
|
84
|
+
const midEdge = Math.floor((lowEdge + highEdge) / 2);
|
|
85
|
+
const output = this.generateDocument({
|
|
86
|
+
opt,
|
|
87
|
+
delim,
|
|
88
|
+
files,
|
|
89
|
+
rankedFiles,
|
|
90
|
+
symbols: [],
|
|
91
|
+
rankedSymbols,
|
|
92
|
+
edges: edges.slice(0, midEdge),
|
|
93
|
+
clusters,
|
|
94
|
+
isTruncated: true,
|
|
95
|
+
truncatedCount: symbols.length,
|
|
96
|
+
originalSymbolsCount: symbols.length,
|
|
97
|
+
isEdgesTruncated: midEdge < edges.length,
|
|
98
|
+
edgesTruncatedCount: edges.length - midEdge
|
|
99
|
+
});
|
|
100
|
+
const estimatedTokens = Math.ceil(Buffer.byteLength(output, "utf8") / 4);
|
|
101
|
+
if (estimatedTokens <= budget) {
|
|
102
|
+
bestEdgeOutput = output;
|
|
103
|
+
lowEdge = midEdge + 1;
|
|
104
|
+
} else {
|
|
105
|
+
highEdge = midEdge - 1;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (bestEdgeOutput) {
|
|
109
|
+
bestOutput = bestEdgeOutput;
|
|
110
|
+
} else {
|
|
111
|
+
bestOutput = this.generateDocument({
|
|
112
|
+
opt,
|
|
113
|
+
delim,
|
|
114
|
+
files: [],
|
|
115
|
+
rankedFiles: [],
|
|
116
|
+
symbols: [],
|
|
117
|
+
rankedSymbols: [],
|
|
118
|
+
edges: [],
|
|
119
|
+
clusters: [],
|
|
120
|
+
isTruncated: true,
|
|
121
|
+
truncatedCount: symbols.length,
|
|
122
|
+
originalSymbolsCount: symbols.length
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return bestOutput.endsWith("\n") ? bestOutput.slice(0, -1) : bestOutput;
|
|
127
|
+
}
|
|
128
|
+
generateDocument(params) {
|
|
129
|
+
const {
|
|
130
|
+
opt,
|
|
131
|
+
delim,
|
|
132
|
+
files,
|
|
133
|
+
rankedFiles,
|
|
134
|
+
symbols,
|
|
135
|
+
rankedSymbols,
|
|
136
|
+
edges,
|
|
137
|
+
clusters,
|
|
138
|
+
isTruncated,
|
|
139
|
+
truncatedCount,
|
|
140
|
+
originalSymbolsCount,
|
|
141
|
+
isEdgesTruncated,
|
|
142
|
+
edgesTruncatedCount
|
|
143
|
+
} = params;
|
|
144
|
+
const parts = [];
|
|
145
|
+
parts.push("version: 1");
|
|
146
|
+
parts.push(`generated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
147
|
+
parts.push(`repo: ${opt.repo || "all"}`);
|
|
148
|
+
parts.push(`tokenBudget: ${opt.tokenBudget || 8192}`);
|
|
149
|
+
parts.push("");
|
|
150
|
+
parts.push("summary:");
|
|
151
|
+
parts.push(` files: ${files.length}`);
|
|
152
|
+
parts.push(` symbols: ${originalSymbolsCount}`);
|
|
153
|
+
parts.push(` edges: ${edges.length}`);
|
|
154
|
+
const languages = Array.from(new Set(files.map((f) => f.language))).filter(Boolean).sort();
|
|
155
|
+
if (languages.length > 0) {
|
|
156
|
+
const escapedLangs = languages.map((l) => toonQuote(l, delim)).join(delim);
|
|
157
|
+
parts.push(` languages[${languages.length}]: ${escapedLangs}`);
|
|
158
|
+
}
|
|
159
|
+
parts.push("");
|
|
160
|
+
const fileRankMap = new Map(rankedFiles.map((f) => [f.path, f.pagerank]));
|
|
161
|
+
const symbolRankMap = new Map(rankedSymbols.map((s) => [`${s.filePath}::${s.name}`, s.pagerank]));
|
|
162
|
+
if (files.length > 0) {
|
|
163
|
+
const sortedFiles = [...files].sort((a, b) => {
|
|
164
|
+
const rA = fileRankMap.get(a.path) || 0;
|
|
165
|
+
const rB = fileRankMap.get(b.path) || 0;
|
|
166
|
+
return rB - rA;
|
|
167
|
+
});
|
|
168
|
+
parts.push(`files[${files.length}]{path,language,symbols,pagerank}:`);
|
|
169
|
+
for (const f of sortedFiles) {
|
|
170
|
+
const path = toonQuote(f.path, delim);
|
|
171
|
+
const language = toonQuote(f.language, delim);
|
|
172
|
+
const symbolsCount = f.lines ? this.store.getSymbolsForFile(f.path).length : 0;
|
|
173
|
+
const rank = formatNumber(parseFloat((fileRankMap.get(f.path) || 0).toFixed(6)));
|
|
174
|
+
parts.push(` ${path}${delim}${language}${delim}${symbolsCount}${delim}${rank}`);
|
|
175
|
+
}
|
|
176
|
+
parts.push("");
|
|
177
|
+
}
|
|
178
|
+
if (symbols.length > 0) {
|
|
179
|
+
parts.push(`symbols[${symbols.length}]{name,kind,file,scope,pagerank}:`);
|
|
180
|
+
for (const s of symbols) {
|
|
181
|
+
const name = toonQuote(s.name, delim);
|
|
182
|
+
const kind = toonQuote(s.kind, delim);
|
|
183
|
+
const file = toonQuote(s.file_path, delim);
|
|
184
|
+
const scope = toonQuote(s.scope || "", delim);
|
|
185
|
+
const rankKey = `${s.file_path}::${s.name}`;
|
|
186
|
+
const rank = formatNumber(parseFloat((symbolRankMap.get(rankKey) || 0).toFixed(6)));
|
|
187
|
+
parts.push(` ${name}${delim}${kind}${delim}${file}${delim}${scope}${delim}${rank}`);
|
|
188
|
+
}
|
|
189
|
+
parts.push("");
|
|
190
|
+
}
|
|
191
|
+
const keptSymbolsSet = new Set(symbols.map((s) => `${s.file_path}::${s.name}`));
|
|
192
|
+
const filteredEdges = edges.filter((e) => {
|
|
193
|
+
if (e.source_symbol) {
|
|
194
|
+
const srcKey = `${e.source_file}::${e.source_symbol}`;
|
|
195
|
+
if (!keptSymbolsSet.has(srcKey)) return false;
|
|
196
|
+
}
|
|
197
|
+
if (e.target_symbol) {
|
|
198
|
+
const tgtKey = `${e.target_file}::${e.target_symbol}`;
|
|
199
|
+
if (!keptSymbolsSet.has(tgtKey)) return false;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
if (filteredEdges.length > 0) {
|
|
204
|
+
parts.push(`edges[${filteredEdges.length}]{sourceFile,targetFile,edgeType,sourceSymbol,targetSymbol,weight}:`);
|
|
205
|
+
for (const e of filteredEdges) {
|
|
206
|
+
const sourceFile = toonQuote(e.source_file, delim);
|
|
207
|
+
const targetFile = toonQuote(e.target_file || "", delim);
|
|
208
|
+
const edgeType = toonQuote(e.edge_type, delim);
|
|
209
|
+
const sourceSymbol = toonQuote(e.source_symbol || "", delim);
|
|
210
|
+
const targetSymbol = toonQuote(e.target_symbol || "", delim);
|
|
211
|
+
const weight = formatNumber(e.weight || 1);
|
|
212
|
+
parts.push(` ${sourceFile}${delim}${targetFile}${delim}${edgeType}${delim}${sourceSymbol}${delim}${targetSymbol}${delim}${weight}`);
|
|
213
|
+
}
|
|
214
|
+
parts.push("");
|
|
215
|
+
}
|
|
216
|
+
if (clusters.length > 0) {
|
|
217
|
+
parts.push(`clusters[${clusters.length}]{name,source,parentName,depth,fileCount}:`);
|
|
218
|
+
for (const c of clusters) {
|
|
219
|
+
const name = toonQuote(c.name, delim);
|
|
220
|
+
const source = toonQuote(c.source, delim);
|
|
221
|
+
const parentName = toonQuote(c.parent_name || "", delim);
|
|
222
|
+
const depth = formatNumber(c.depth || 0);
|
|
223
|
+
const fileCount = formatNumber(c.file_count || 0);
|
|
224
|
+
parts.push(` ${name}${delim}${source}${delim}${parentName}${delim}${depth}${delim}${fileCount}`);
|
|
225
|
+
}
|
|
226
|
+
parts.push("");
|
|
227
|
+
}
|
|
228
|
+
if (isTruncated) {
|
|
229
|
+
parts.push("truncated: true");
|
|
230
|
+
parts.push("truncatedAt: symbols");
|
|
231
|
+
parts.push(`includedSymbols: ${symbols.length}`);
|
|
232
|
+
parts.push(`totalSymbols: ${originalSymbolsCount}`);
|
|
233
|
+
if (isEdgesTruncated) {
|
|
234
|
+
parts.push("edgesTruncated: true");
|
|
235
|
+
parts.push(`includedEdges: ${filteredEdges.length}`);
|
|
236
|
+
parts.push(`totalEdges: ${edges.length}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return parts.join("\n");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
export {
|
|
243
|
+
ToonExporter,
|
|
244
|
+
formatNumber,
|
|
245
|
+
toonQuote
|
|
246
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { FrameworkDetector, ScanContext, RouteBinding } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
declare class AspNetDetector implements FrameworkDetector {
|
|
4
|
+
readonly name = "aspnet";
|
|
5
|
+
readonly language = "csharp";
|
|
6
|
+
readonly filePattern: RegExp;
|
|
7
|
+
detect(projectRoot: string, files: string[]): Promise<boolean>;
|
|
8
|
+
extractRoutes(filePath: string, content: string, ctx: ScanContext): Promise<RouteBinding[]>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { AspNetDetector };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class AspNetDetector {
|
|
2
|
+
name = "aspnet";
|
|
3
|
+
language = "csharp";
|
|
4
|
+
filePattern = /\.cs$/;
|
|
5
|
+
async detect(projectRoot, files) {
|
|
6
|
+
const hasCsproj = files.some((f) => f.endsWith(".csproj") || f.endsWith(".sln"));
|
|
7
|
+
if (hasCsproj) return true;
|
|
8
|
+
return files.some((f) => f.endsWith(".cs"));
|
|
9
|
+
}
|
|
10
|
+
async extractRoutes(filePath, content, ctx) {
|
|
11
|
+
const routes = [];
|
|
12
|
+
if (!content.includes("Controller") && !content.includes("Route") && !content.includes("Http")) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const classMatch = content.match(/\bclass\s+([a-zA-Z0-9_]+)\b/);
|
|
16
|
+
if (!classMatch) return [];
|
|
17
|
+
const className = classMatch[1];
|
|
18
|
+
const controllerName = className.endsWith("Controller") ? className.substring(0, className.length - 10).toLowerCase() : className.toLowerCase();
|
|
19
|
+
let classRoutePrefix = "";
|
|
20
|
+
const classRouteMatch = content.match(/\[Route\s*\(\s*['"]([^'"]+)['"]\s*\)\]/);
|
|
21
|
+
if (classRouteMatch) {
|
|
22
|
+
classRoutePrefix = classRouteMatch[1];
|
|
23
|
+
classRoutePrefix = classRoutePrefix.replace(/\[controller\]/gi, controllerName);
|
|
24
|
+
}
|
|
25
|
+
const methodRouteRegex = /\[Http(Get|Post|Put|Delete|Patch)(?:\s*\(\s*['"]([^'"]+)['"]\s*\))?\][^({]*?\b([a-zA-Z0-9_]+)\s*\(/g;
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = methodRouteRegex.exec(content)) !== null) {
|
|
28
|
+
const verb = match[1].toUpperCase();
|
|
29
|
+
const methodTemplate = match[2] || "";
|
|
30
|
+
const methodName = match[3];
|
|
31
|
+
const cleanClassPrefix = classRoutePrefix.replace(/^\/|\/$/g, "");
|
|
32
|
+
const cleanMethodTemplate = methodTemplate.replace(/^\/|\/$/g, "");
|
|
33
|
+
let combinedPath = "/" + [cleanClassPrefix, cleanMethodTemplate].filter(Boolean).join("/");
|
|
34
|
+
combinedPath = combinedPath.replace(/\{([a-zA-Z0-9_?*]+)(?::[a-zA-Z0-9_]+)?\}/g, "{$1}");
|
|
35
|
+
routes.push({
|
|
36
|
+
framework: this.name,
|
|
37
|
+
method: verb,
|
|
38
|
+
path: combinedPath,
|
|
39
|
+
handlerFile: filePath,
|
|
40
|
+
handlerSymbol: `${className}.${methodName}`,
|
|
41
|
+
metadata: {
|
|
42
|
+
confidence: "inferred",
|
|
43
|
+
routeType: "server"
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return routes;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
AspNetDetector
|
|
52
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FrameworkDetector, ScanContext, RouteBinding } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
declare class DjangoDetector implements FrameworkDetector {
|
|
4
|
+
readonly name = "django";
|
|
5
|
+
readonly language = "python";
|
|
6
|
+
readonly filePattern: RegExp;
|
|
7
|
+
private projectFiles;
|
|
8
|
+
detect(projectRoot: string, files: string[]): Promise<boolean>;
|
|
9
|
+
extractRoutes(filePath: string, content: string, ctx: ScanContext): Promise<RouteBinding[]>;
|
|
10
|
+
private parseUrlsFile;
|
|
11
|
+
private resolveModulePath;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { DjangoDetector };
|