@mauroandre/velojs 0.0.39 → 0.0.41
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/agent/skills/velojs-project-graph/SKILL.md +30 -5
- package/dist/cli.js +30 -25
- package/dist/graph-viewer.d.ts +13 -0
- package/dist/graph-viewer.js +418 -0
- package/dist/graph.js +1 -1
- package/dist/vite.js +1 -1
- package/package.json +1 -1
|
@@ -9,11 +9,7 @@ VeloJS can generate a structural snapshot of your project — the route tree hie
|
|
|
9
9
|
|
|
10
10
|
## When to use
|
|
11
11
|
|
|
12
|
-
Always. Before reading any source file in a VeloJS project,
|
|
13
|
-
|
|
14
|
-
- **Route tree** — full hierarchy of layouts, pages, paths, `isRoot`, and middlewares
|
|
15
|
-
- **Module dependencies** — for every module: what it imports (`imports`) and who imports it (`importedBy`)
|
|
16
|
-
- **Convention exports** — which modules export `loader`, `action_*`, `stream_*`, `socket_*`
|
|
12
|
+
Always. Before reading any source file in a VeloJS project, query `.velojs/graph.json` for the specific information you need. The full file can be large; use `jq` for targeted queries.
|
|
17
13
|
|
|
18
14
|
## Generating the graph
|
|
19
15
|
|
|
@@ -27,6 +23,35 @@ This runs a fast Babel-based crawl of `app/routes.tsx` and its transitive import
|
|
|
27
23
|
|
|
28
24
|
The graph is also regenerated automatically on every `velojs build` (via the `velo:graph` plugin) and on every file change during `velojs dev`.
|
|
29
25
|
|
|
26
|
+
## Querying the graph
|
|
27
|
+
|
|
28
|
+
The graph is a compact JSON file (no whitespace). Do not read it in full — use `jq` for targeted queries:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# A single module — what it exports, what it imports, who imports it
|
|
32
|
+
jq '.modules["auth/Login"]' .velojs/graph.json
|
|
33
|
+
|
|
34
|
+
# The route tree (hierarchy of layouts + pages with paths and middlewares)
|
|
35
|
+
jq '.routes' .velojs/graph.json
|
|
36
|
+
|
|
37
|
+
# All modules that export a loader
|
|
38
|
+
jq '.modules | to_entries[] | select(.value.exports.hasLoader) | .key' .velojs/graph.json
|
|
39
|
+
|
|
40
|
+
# Impact analysis: what breaks if I change this service?
|
|
41
|
+
jq '.modules["modules/respondent/respondent.service"] | .importedBy' .velojs/graph.json
|
|
42
|
+
|
|
43
|
+
# All actions in the project (module + action name)
|
|
44
|
+
jq '.modules | to_entries[] | .key as $k | .value.exports.actions[] | "\($k): action_\(.)"' .velojs/graph.json
|
|
45
|
+
|
|
46
|
+
# Modules imported by a specific page
|
|
47
|
+
jq '.modules["admin/DepartmentDetail"].imports' .velojs/graph.json
|
|
48
|
+
|
|
49
|
+
# Which pages use a shared component?
|
|
50
|
+
jq -r '.modules["components/Inputs"].importedBy[]' .velojs/graph.json \
|
|
51
|
+
| while read id; do jq -r ".modules[\"$id\"].kind" .velojs/graph.json; done \
|
|
52
|
+
| sort | uniq -c
|
|
53
|
+
```
|
|
54
|
+
|
|
30
55
|
## Graph structure
|
|
31
56
|
|
|
32
57
|
```typescript
|
package/dist/cli.js
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import { spawnSync as
|
|
1
|
+
import { spawnSync as c } from "node:child_process";
|
|
2
2
|
import { join as n, resolve as u } from "node:path";
|
|
3
3
|
import p from "node:fs";
|
|
4
|
-
const
|
|
5
|
-
const t =
|
|
4
|
+
const o = process.argv.slice(2), a = o[0], v = (e, s = []) => {
|
|
5
|
+
const t = c(e, s, {
|
|
6
6
|
stdio: "inherit",
|
|
7
7
|
cwd: process.cwd()
|
|
8
8
|
});
|
|
9
9
|
process.exit(t.status ?? 0);
|
|
10
|
-
},
|
|
11
|
-
v("npx", ["vite", ...
|
|
12
|
-
},
|
|
10
|
+
}, m = () => {
|
|
11
|
+
v("npx", ["vite", ...o.slice(1)]);
|
|
12
|
+
}, j = (e) => {
|
|
13
13
|
const s = Date.now().toString(36), t = {
|
|
14
14
|
...process.env,
|
|
15
15
|
VELO_BUILD_HASH: s,
|
|
16
16
|
...e ? { VELO_STATIC: "1" } : {}
|
|
17
17
|
};
|
|
18
18
|
console.log("Building client...");
|
|
19
|
-
const
|
|
19
|
+
const r = c("npx", ["vite", "build"], {
|
|
20
20
|
stdio: "inherit",
|
|
21
21
|
cwd: process.cwd(),
|
|
22
22
|
env: t
|
|
23
23
|
});
|
|
24
|
-
|
|
25
|
-
const i =
|
|
24
|
+
r.status !== 0 && (console.error("Client build failed. Aborting."), process.exit(r.status ?? 1)), console.log("Building server...");
|
|
25
|
+
const i = c("npx", ["vite", "build", "--mode", "server"], {
|
|
26
26
|
stdio: "inherit",
|
|
27
27
|
cwd: process.cwd(),
|
|
28
28
|
env: t
|
|
29
29
|
});
|
|
30
|
-
i.status !== 0 && (console.error("Server build failed. Aborting."), process.exit(i.status ?? 1)), console.log("Build complete!"), e &&
|
|
31
|
-
},
|
|
30
|
+
i.status !== 0 && (console.error("Server build failed. Aborting."), process.exit(i.status ?? 1)), console.log("Build complete!"), e && b();
|
|
31
|
+
}, b = async () => {
|
|
32
32
|
process.env.NODE_ENV = "production", process.env.VELO_STATIC = "1";
|
|
33
|
-
const s = await import(u(process.cwd(), "dist/server.js")), t = s.default,
|
|
34
|
-
(!t || !
|
|
33
|
+
const s = await import(u(process.cwd(), "dist/server.js")), t = s.default, r = s.routes;
|
|
34
|
+
(!t || !r) && (console.error("Failed to load app or routes from dist/server.js"), process.exit(1));
|
|
35
35
|
const { generateStatic: i } = await import("./static.js");
|
|
36
|
-
await i(t,
|
|
37
|
-
},
|
|
36
|
+
await i(t, r);
|
|
37
|
+
}, f = () => {
|
|
38
38
|
process.env.NODE_ENV = "production";
|
|
39
39
|
const e = u(process.cwd(), "dist/server.js");
|
|
40
40
|
v("node", [e]);
|
|
@@ -52,6 +52,7 @@ Commands:
|
|
|
52
52
|
build --static Build as static site (HTML + JSON)
|
|
53
53
|
start Start production server
|
|
54
54
|
graph [app-dir] Generate .velojs/graph.json (route tree + dependency graph)
|
|
55
|
+
graph --view Generate the graph and open an interactive viewer
|
|
55
56
|
|
|
56
57
|
Examples:
|
|
57
58
|
velojs init my-app
|
|
@@ -61,30 +62,34 @@ Examples:
|
|
|
61
62
|
velojs start
|
|
62
63
|
velojs graph
|
|
63
64
|
velojs graph src/app
|
|
65
|
+
velojs graph --view
|
|
64
66
|
`);
|
|
65
67
|
};
|
|
66
|
-
switch (
|
|
68
|
+
switch (a) {
|
|
67
69
|
case "init": {
|
|
68
70
|
const { runInit: e } = await import("./init.js");
|
|
69
|
-
await e(
|
|
71
|
+
await e(o[1]);
|
|
70
72
|
break;
|
|
71
73
|
}
|
|
72
74
|
case "dev":
|
|
73
|
-
|
|
75
|
+
m();
|
|
74
76
|
break;
|
|
75
77
|
case "build": {
|
|
76
|
-
const e =
|
|
77
|
-
|
|
78
|
+
const e = o.includes("--static");
|
|
79
|
+
j(e);
|
|
78
80
|
break;
|
|
79
81
|
}
|
|
80
82
|
case "start":
|
|
81
|
-
|
|
83
|
+
f();
|
|
82
84
|
break;
|
|
83
85
|
case "graph": {
|
|
84
|
-
const { buildGraph: e } = await import("./graph.js"), s = process.cwd(), t =
|
|
86
|
+
const { buildGraph: e } = await import("./graph.js"), s = process.cwd(), t = o[1] && !o[1].startsWith("-") ? o[1] : null, r = n(s, t ?? "app"), i = e(r), l = n(s, ".velojs");
|
|
85
87
|
p.mkdirSync(l, { recursive: !0 });
|
|
86
|
-
const
|
|
87
|
-
p.writeFileSync(
|
|
88
|
+
const w = n(l, "graph.json");
|
|
89
|
+
if (p.writeFileSync(w, JSON.stringify(i)), console.log("Graph written to .velojs/graph.json"), o.includes("--view")) {
|
|
90
|
+
const { serveGraphViewer: g } = await import("./graph-viewer.js"), h = await g(s);
|
|
91
|
+
console.log(`Graph viewer running at ${h} (Ctrl+C to stop)`);
|
|
92
|
+
}
|
|
88
93
|
break;
|
|
89
94
|
}
|
|
90
95
|
case "help":
|
|
@@ -93,5 +98,5 @@ switch (c) {
|
|
|
93
98
|
d();
|
|
94
99
|
break;
|
|
95
100
|
default:
|
|
96
|
-
|
|
101
|
+
a && console.error(`Unknown command: ${a}`), d(), process.exit(1);
|
|
97
102
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VeloJS Graph Viewer
|
|
3
|
+
*
|
|
4
|
+
* Serves an interactive, self-contained HTML visualization of
|
|
5
|
+
* .velojs/graph.json — route tree + force-directed dependency graph.
|
|
6
|
+
* Plain node:http, no dependencies. The graph JSON is read from disk
|
|
7
|
+
* on every request so the view is always fresh.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Starts the graph viewer server. Reads `.velojs/graph.json` from
|
|
11
|
+
* projectRoot on every request — always fresh. Returns the URL.
|
|
12
|
+
*/
|
|
13
|
+
export declare function serveGraphViewer(projectRoot: string): Promise<string>;
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import r from "node:http";
|
|
2
|
+
import d from "node:fs";
|
|
3
|
+
import s from "node:path";
|
|
4
|
+
const l = `<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="utf-8">
|
|
8
|
+
<title>VeloJS Graph</title>
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
|
+
<style>
|
|
11
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
12
|
+
body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #0b0e14; color: #d7dde8; overflow: hidden; }
|
|
13
|
+
#topbar { display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: #11151f; border-bottom: 1px solid #232a3a; }
|
|
14
|
+
#topbar h1 { font-size: 14px; font-weight: 600; color: #e8edf5; }
|
|
15
|
+
#topbar h1 span { color: #5b8def; }
|
|
16
|
+
.tab { padding: 5px 14px; border-radius: 6px; border: 1px solid #2a3247; background: transparent; color: #9aa5b8; font: inherit; font-size: 12px; cursor: pointer; }
|
|
17
|
+
.tab.active { background: #1d2740; color: #e8edf5; border-color: #3d5a99; }
|
|
18
|
+
#search { margin-left: auto; padding: 5px 10px; border-radius: 6px; border: 1px solid #2a3247; background: #0b0e14; color: #d7dde8; font: inherit; font-size: 12px; width: 220px; }
|
|
19
|
+
#search:focus { outline: none; border-color: #3d5a99; }
|
|
20
|
+
#canvas { display: block; cursor: grab; }
|
|
21
|
+
#panel { position: fixed; top: 46px; right: 0; width: 330px; bottom: 0; background: #11151f; border-left: 1px solid #232a3a; padding: 16px; overflow-y: auto; transform: translateX(100%); transition: transform .18s ease; font-size: 12px; }
|
|
22
|
+
#panel.open { transform: translateX(0); }
|
|
23
|
+
#panel h2 { font-size: 14px; color: #e8edf5; word-break: break-all; margin-bottom: 4px; }
|
|
24
|
+
#panel .file { color: #6b768c; font-size: 11px; margin-bottom: 12px; word-break: break-all; }
|
|
25
|
+
#panel h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: #6b768c; margin: 14px 0 6px; }
|
|
26
|
+
#panel li { list-style: none; padding: 3px 0; color: #aebad0; word-break: break-all; cursor: pointer; }
|
|
27
|
+
#panel li:hover { color: #5b8def; }
|
|
28
|
+
#panel li.ext { color: #6b768c; cursor: default; }
|
|
29
|
+
#panel li.ext:hover { color: #6b768c; }
|
|
30
|
+
.badge { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: 10px; margin-right: 5px; background: #1d2740; color: #9db8ef; border: 1px solid #2f3d5f; }
|
|
31
|
+
#legend { position: fixed; left: 14px; bottom: 14px; background: #11151fdd; border: 1px solid #232a3a; border-radius: 8px; padding: 10px 14px; font-size: 11px; color: #9aa5b8; }
|
|
32
|
+
#legend i { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 6px; }
|
|
33
|
+
#hint { position: fixed; left: 50%; transform: translateX(-50%); bottom: 14px; font-size: 11px; color: #55607a; }
|
|
34
|
+
</style>
|
|
35
|
+
</head>
|
|
36
|
+
<body>
|
|
37
|
+
<div id="topbar">
|
|
38
|
+
<h1>velo<span>js</span> graph</h1>
|
|
39
|
+
<button class="tab active" id="tab-routes">Route tree</button>
|
|
40
|
+
<button class="tab" id="tab-modules">Dependencies</button>
|
|
41
|
+
<input id="search" placeholder="filter modules... (/)">
|
|
42
|
+
</div>
|
|
43
|
+
<canvas id="canvas"></canvas>
|
|
44
|
+
<div id="panel"></div>
|
|
45
|
+
<div id="legend">
|
|
46
|
+
<div><i style="background:#f59e0b"></i>root</div>
|
|
47
|
+
<div><i style="background:#a78bfa"></i>layout</div>
|
|
48
|
+
<div><i style="background:#5b8def"></i>page</div>
|
|
49
|
+
<div><i style="background:#4b5565"></i>module</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div id="hint">drag to pan · scroll to zoom · drag nodes · hover = impact · click = focus neighborhood · / = search · esc = reset</div>
|
|
52
|
+
<script>
|
|
53
|
+
var KIND_COLORS = { root: "#f59e0b", layout: "#a78bfa", page: "#5b8def", unknown: "#4b5565" };
|
|
54
|
+
|
|
55
|
+
var graph = null;
|
|
56
|
+
var view = "routes";
|
|
57
|
+
var cam = { x: 0, y: 0, k: 1 };
|
|
58
|
+
var canvas = document.getElementById("canvas");
|
|
59
|
+
var ctx = canvas.getContext("2d");
|
|
60
|
+
var W = 0, H = 0;
|
|
61
|
+
|
|
62
|
+
function resize() {
|
|
63
|
+
W = window.innerWidth; H = window.innerHeight;
|
|
64
|
+
canvas.width = W * devicePixelRatio; canvas.height = H * devicePixelRatio;
|
|
65
|
+
canvas.style.width = W + "px"; canvas.style.height = H + "px";
|
|
66
|
+
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
|
67
|
+
}
|
|
68
|
+
window.addEventListener("resize", resize); resize();
|
|
69
|
+
|
|
70
|
+
// ---------- data ----------
|
|
71
|
+
var modNodes = [], modEdges = [], modById = {};
|
|
72
|
+
var routeNodes = [], routeEdges = [];
|
|
73
|
+
|
|
74
|
+
function buildModuleGraph() {
|
|
75
|
+
var ids = Object.keys(graph.modules);
|
|
76
|
+
ids.forEach(function (id, i) {
|
|
77
|
+
var m = graph.modules[id];
|
|
78
|
+
modNodes.push({ id: id, kind: m.kind, exports: m.exports, imports: m.imports, importedBy: m.importedBy, file: m.file,
|
|
79
|
+
x: (Math.random() - 0.5) * 800, y: (Math.random() - 0.5) * 800, vx: 0, vy: 0 });
|
|
80
|
+
});
|
|
81
|
+
modNodes.forEach(function (n) { modById[n.id] = n; });
|
|
82
|
+
modNodes.forEach(function (n) {
|
|
83
|
+
n.imports.forEach(function (imp) {
|
|
84
|
+
if (modById[imp]) modEdges.push({ from: n, to: modById[imp] });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildRouteLayout() {
|
|
90
|
+
var leafCount = 0;
|
|
91
|
+
// tidy-ish: leaves get sequential x; parents sit at the midpoint of children
|
|
92
|
+
function layout(node, depth, parent) {
|
|
93
|
+
var rn = { data: node, depth: depth, x: 0, y: depth, parent: parent, kids: [] };
|
|
94
|
+
routeNodes.push(rn);
|
|
95
|
+
if (parent) { parent.kids.push(rn); routeEdges.push({ from: parent, to: rn }); }
|
|
96
|
+
if (!node.children.length) { rn.x = leafCount++; return rn; }
|
|
97
|
+
var xs = node.children.map(function (c) { return layout(c, depth + 1, rn).x; });
|
|
98
|
+
rn.x = (xs[0] + xs[xs.length - 1]) / 2;
|
|
99
|
+
return rn;
|
|
100
|
+
}
|
|
101
|
+
graph.routes.forEach(function (r) { layout(r, 0, null); });
|
|
102
|
+
var gapX = 250, gapY = 130;
|
|
103
|
+
var minX = Math.min.apply(null, routeNodes.map(function (n) { return n.x; }));
|
|
104
|
+
routeNodes.forEach(function (n) { n.px = (n.x - minX) * gapX + 80; n.py = n.y * gapY + 80; });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------- camera helpers ----------
|
|
108
|
+
function fitView(xs, ys) {
|
|
109
|
+
var minX = Math.min.apply(null, xs), maxX = Math.max.apply(null, xs);
|
|
110
|
+
var minY = Math.min.apply(null, ys), maxY = Math.max.apply(null, ys);
|
|
111
|
+
var w = Math.max(maxX - minX, 1), h = Math.max(maxY - minY, 1);
|
|
112
|
+
cam.k = Math.min((W - 120) / w, (H - 120) / h, 1.4);
|
|
113
|
+
cam.x = W / 2 - (minX + w / 2) * cam.k;
|
|
114
|
+
cam.y = H / 2 - (minY + h / 2) * cam.k;
|
|
115
|
+
}
|
|
116
|
+
function toScreen(x, y) { return [x * cam.k + cam.x, y * cam.k + cam.y]; }
|
|
117
|
+
function toWorld(sx, sy) { return [(sx - cam.x) / cam.k, (sy - cam.y) / cam.k]; }
|
|
118
|
+
|
|
119
|
+
// ---------- force simulation (modules) ----------
|
|
120
|
+
var alpha = 1;
|
|
121
|
+
var MAXV = 25;
|
|
122
|
+
function tick() {
|
|
123
|
+
if (alpha < 0.005) return;
|
|
124
|
+
alpha *= 0.995;
|
|
125
|
+
var i, j, n, m, dx, dy, d2, f;
|
|
126
|
+
for (i = 0; i < modNodes.length; i++) {
|
|
127
|
+
n = modNodes[i];
|
|
128
|
+
for (j = i + 1; j < modNodes.length; j++) {
|
|
129
|
+
m = modNodes[j];
|
|
130
|
+
dx = n.x - m.x; dy = n.y - m.y;
|
|
131
|
+
d2 = dx * dx + dy * dy + 0.01;
|
|
132
|
+
f = 2600 / d2 * alpha;
|
|
133
|
+
var d = Math.sqrt(d2);
|
|
134
|
+
n.vx += dx / d * f; n.vy += dy / d * f;
|
|
135
|
+
m.vx -= dx / d * f; m.vy -= dy / d * f;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
modEdges.forEach(function (e) {
|
|
139
|
+
dx = e.to.x - e.from.x; dy = e.to.y - e.from.y;
|
|
140
|
+
var d = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
141
|
+
f = (d - 110) * 0.02 * alpha;
|
|
142
|
+
e.from.vx += dx / d * f * d * 0.02; e.from.vy += dy / d * f * d * 0.02;
|
|
143
|
+
e.to.vx -= dx / d * f * d * 0.02; e.to.vy -= dy / d * f * d * 0.02;
|
|
144
|
+
});
|
|
145
|
+
modNodes.forEach(function (n) {
|
|
146
|
+
if (n === dragNode) return;
|
|
147
|
+
n.vx += -n.x * 0.002 * alpha; n.vy += -n.y * 0.002 * alpha;
|
|
148
|
+
n.vx *= 0.85; n.vy *= 0.85;
|
|
149
|
+
if (n.vx > MAXV) n.vx = MAXV; else if (n.vx < -MAXV) n.vx = -MAXV;
|
|
150
|
+
if (n.vy > MAXV) n.vy = MAXV; else if (n.vy < -MAXV) n.vy = -MAXV;
|
|
151
|
+
n.x += n.vx; n.y += n.vy;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------- focus mode ----------
|
|
156
|
+
// Click isolates a node's neighborhood: transitive imports + importedBy
|
|
157
|
+
// (modules), or ancestors + subtree (routes). Click empty / ESC exits.
|
|
158
|
+
var focusModSet = null, focusRouteSet = null;
|
|
159
|
+
function computeFocusModules(id) {
|
|
160
|
+
var set = {}, q = [id];
|
|
161
|
+
set[id] = true;
|
|
162
|
+
while (q.length) {
|
|
163
|
+
var m = graph.modules[q.shift()];
|
|
164
|
+
if (!m) continue;
|
|
165
|
+
m.imports.forEach(function (i) { if (modById[i] && !set[i]) { set[i] = true; q.push(i); } });
|
|
166
|
+
m.importedBy.forEach(function (i) { if (modById[i] && !set[i]) { set[i] = true; q.push(i); } });
|
|
167
|
+
}
|
|
168
|
+
return set;
|
|
169
|
+
}
|
|
170
|
+
function computeFocusRoutes(rn) {
|
|
171
|
+
var set = [];
|
|
172
|
+
var p = rn.parent;
|
|
173
|
+
while (p) { set.push(p); p = p.parent; }
|
|
174
|
+
(function down(n) { set.push(n); n.kids.forEach(down); })(rn);
|
|
175
|
+
return set;
|
|
176
|
+
}
|
|
177
|
+
function modVisible(n) { return matches(n.id) && (!focusModSet || focusModSet[n.id]); }
|
|
178
|
+
function routeVisible(n) { return matches(n.data.moduleId || "(wrapper)") && (!focusRouteSet || focusRouteSet.indexOf(n) >= 0); }
|
|
179
|
+
|
|
180
|
+
// ---------- impact highlight ----------
|
|
181
|
+
var hoverNode = null;
|
|
182
|
+
var impactSet = null;
|
|
183
|
+
function computeImpact(n) {
|
|
184
|
+
var set = {}, queue = [n.id];
|
|
185
|
+
set[n.id] = 2;
|
|
186
|
+
while (queue.length) {
|
|
187
|
+
var id = queue.shift();
|
|
188
|
+
var mod = graph.modules[id];
|
|
189
|
+
if (!mod) continue;
|
|
190
|
+
mod.importedBy.forEach(function (up) {
|
|
191
|
+
if (!(up in set)) { set[up] = 1; queue.push(up); }
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return set;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---------- selection / panel ----------
|
|
198
|
+
var selected = null;
|
|
199
|
+
var panel = document.getElementById("panel");
|
|
200
|
+
function showPanel(id) {
|
|
201
|
+
selected = id;
|
|
202
|
+
var m = graph.modules[id];
|
|
203
|
+
if (!m) { panel.className = ""; panel.innerHTML = ""; return; }
|
|
204
|
+
var b = [];
|
|
205
|
+
if (m.exports.hasComponent) b.push('<span class="badge">Component</span>');
|
|
206
|
+
if (m.exports.hasLoader) b.push('<span class="badge">loader</span>');
|
|
207
|
+
m.exports.actions.forEach(function (a) { b.push('<span class="badge">action_' + a + "</span>"); });
|
|
208
|
+
m.exports.streams.forEach(function (s) { b.push('<span class="badge">stream_' + s + "</span>"); });
|
|
209
|
+
m.exports.sockets.forEach(function (s) { b.push('<span class="badge">socket_' + s + "</span>"); });
|
|
210
|
+
var html = "<h2>" + id + '</h2><div class="file">' + m.file + " · " + m.kind + "</div><div>" + b.join("") + "</div>";
|
|
211
|
+
html += "<h3>imports (" + m.imports.length + ")</h3><ul>";
|
|
212
|
+
m.imports.forEach(function (i) {
|
|
213
|
+
html += modById[i] ? "<li data-id='" + i + "'>→ " + i + "</li>" : "<li class='ext'>→ " + i + "</li>";
|
|
214
|
+
});
|
|
215
|
+
html += "</ul><h3>imported by (" + m.importedBy.length + ")</h3><ul>";
|
|
216
|
+
m.importedBy.forEach(function (i) { html += "<li data-id='" + i + "'>← " + i + "</li>"; });
|
|
217
|
+
html += "</ul>";
|
|
218
|
+
panel.innerHTML = html;
|
|
219
|
+
panel.className = "open";
|
|
220
|
+
panel.querySelectorAll("li[data-id]").forEach(function (li) {
|
|
221
|
+
li.addEventListener("click", function () { showPanel(li.getAttribute("data-id")); focusModule(li.getAttribute("data-id")); });
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function focusModule(id) {
|
|
225
|
+
var n = modById[id];
|
|
226
|
+
if (!n) return;
|
|
227
|
+
if (view !== "modules") setView("modules");
|
|
228
|
+
cam.x = W / 2 - n.x * cam.k; cam.y = H / 2 - n.y * cam.k;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------- search ----------
|
|
232
|
+
var query = "";
|
|
233
|
+
document.getElementById("search").addEventListener("input", function (e) { query = e.target.value.toLowerCase(); });
|
|
234
|
+
document.addEventListener("keydown", function (e) {
|
|
235
|
+
if (e.key === "/" && document.activeElement !== document.getElementById("search")) { e.preventDefault(); document.getElementById("search").focus(); }
|
|
236
|
+
if (e.key === "Escape") { panel.className = ""; selected = null; query = ""; document.getElementById("search").value = ""; focusModSet = null; focusRouteSet = null; }
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ---------- interaction ----------
|
|
240
|
+
var dragNode = null, panning = false, lastX = 0, lastY = 0, moved = false;
|
|
241
|
+
function nodeAt(sx, sy) {
|
|
242
|
+
var w = toWorld(sx, sy);
|
|
243
|
+
var list = view === "modules" ? modNodes : routeNodes;
|
|
244
|
+
for (var i = list.length - 1; i >= 0; i--) {
|
|
245
|
+
var n = list[i];
|
|
246
|
+
if (view === "modules" ? !modVisible(n) : !routeVisible(n)) continue;
|
|
247
|
+
var nx = view === "modules" ? n.x : n.px, ny = view === "modules" ? n.y : n.py;
|
|
248
|
+
var dx = w[0] - nx, dy = w[1] - ny;
|
|
249
|
+
if (dx * dx + dy * dy < 14 * 14 / (cam.k * cam.k) + 36) return n;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
canvas.addEventListener("mousedown", function (e) {
|
|
254
|
+
var n = nodeAt(e.clientX, e.clientY);
|
|
255
|
+
if (n && view === "modules") { dragNode = n; alpha = Math.max(alpha, 0.3); }
|
|
256
|
+
else panning = true;
|
|
257
|
+
lastX = e.clientX; lastY = e.clientY; moved = false;
|
|
258
|
+
});
|
|
259
|
+
canvas.addEventListener("mousemove", function (e) {
|
|
260
|
+
var dx = e.clientX - lastX, dy = e.clientY - lastY;
|
|
261
|
+
if (Math.abs(dx) + Math.abs(dy) > 3) moved = true;
|
|
262
|
+
if (dragNode) { var w = toWorld(e.clientX, e.clientY); dragNode.x = w[0]; dragNode.y = w[1]; dragNode.vx = dragNode.vy = 0; }
|
|
263
|
+
else if (panning) { cam.x += dx; cam.y += dy; }
|
|
264
|
+
else {
|
|
265
|
+
var n = nodeAt(e.clientX, e.clientY);
|
|
266
|
+
hoverNode = view === "modules" ? n : null;
|
|
267
|
+
impactSet = hoverNode ? computeImpact(hoverNode) : null;
|
|
268
|
+
canvas.style.cursor = n ? "pointer" : "grab";
|
|
269
|
+
}
|
|
270
|
+
lastX = e.clientX; lastY = e.clientY;
|
|
271
|
+
});
|
|
272
|
+
canvas.addEventListener("mouseup", function (e) {
|
|
273
|
+
if (!moved) {
|
|
274
|
+
var n = nodeAt(e.clientX, e.clientY);
|
|
275
|
+
if (view === "modules") {
|
|
276
|
+
showPanel(n ? n.id : null);
|
|
277
|
+
focusModSet = n ? computeFocusModules(n.id) : null;
|
|
278
|
+
} else {
|
|
279
|
+
if (n && n.data.moduleId) showPanel(n.data.moduleId);
|
|
280
|
+
focusRouteSet = n ? computeFocusRoutes(n) : null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
dragNode = null; panning = false;
|
|
284
|
+
});
|
|
285
|
+
canvas.addEventListener("wheel", function (e) {
|
|
286
|
+
e.preventDefault();
|
|
287
|
+
var k = Math.exp(-e.deltaY * 0.0012);
|
|
288
|
+
var w = toWorld(e.clientX, e.clientY);
|
|
289
|
+
cam.k *= k;
|
|
290
|
+
cam.x = e.clientX - w[0] * cam.k; cam.y = e.clientY - w[1] * cam.k;
|
|
291
|
+
}, { passive: false });
|
|
292
|
+
|
|
293
|
+
// ---------- rendering ----------
|
|
294
|
+
function matches(id) { return !query || id.toLowerCase().indexOf(query) >= 0; }
|
|
295
|
+
|
|
296
|
+
function drawRoutes() {
|
|
297
|
+
ctx.clearRect(0, 0, W, H);
|
|
298
|
+
ctx.strokeStyle = "#2a3247"; ctx.lineWidth = 1.2;
|
|
299
|
+
routeEdges.forEach(function (e) {
|
|
300
|
+
if (!routeVisible(e.from) || !routeVisible(e.to)) return;
|
|
301
|
+
var a = toScreen(e.from.px, e.from.py), b = toScreen(e.to.px, e.to.py);
|
|
302
|
+
ctx.beginPath();
|
|
303
|
+
ctx.moveTo(a[0], a[1]);
|
|
304
|
+
ctx.bezierCurveTo(a[0], (a[1] + b[1]) / 2, b[0], (a[1] + b[1]) / 2, b[0], b[1]);
|
|
305
|
+
ctx.stroke();
|
|
306
|
+
});
|
|
307
|
+
routeNodes.forEach(function (n) {
|
|
308
|
+
var d = n.data;
|
|
309
|
+
var id = d.moduleId || "(wrapper)";
|
|
310
|
+
if (!routeVisible(n)) return;
|
|
311
|
+
var p = toScreen(n.px, n.py);
|
|
312
|
+
var color = d.isRoot ? KIND_COLORS.root : (d.children.length ? KIND_COLORS.layout : KIND_COLORS.page);
|
|
313
|
+
var sel = selected === d.moduleId;
|
|
314
|
+
ctx.beginPath(); ctx.arc(p[0], p[1], sel ? 9 : 6, 0, 7);
|
|
315
|
+
ctx.fillStyle = color; ctx.fill();
|
|
316
|
+
if (sel) { ctx.strokeStyle = "#e8edf5"; ctx.lineWidth = 2; ctx.stroke(); }
|
|
317
|
+
// level-of-detail: labels only when zoomed in enough, otherwise the
|
|
318
|
+
// overview stays clean (dots only). fullPath needs even more zoom.
|
|
319
|
+
if (cam.k >= 0.55 || sel) {
|
|
320
|
+
ctx.fillStyle = "#d7dde8"; ctx.font = "11px ui-monospace, monospace"; ctx.textAlign = "center";
|
|
321
|
+
ctx.fillText(id.split("/").pop(), p[0], p[1] - 13);
|
|
322
|
+
}
|
|
323
|
+
if (cam.k >= 0.85 || sel) {
|
|
324
|
+
ctx.fillStyle = "#55607a"; ctx.font = "10px ui-monospace, monospace";
|
|
325
|
+
var sub = d.fullPath || "";
|
|
326
|
+
if (d.middlewares.length) sub += " · " + d.middlewares.join(", ");
|
|
327
|
+
ctx.fillText(sub, p[0], p[1] + 20);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function drawModules() {
|
|
333
|
+
ctx.clearRect(0, 0, W, H);
|
|
334
|
+
ctx.strokeStyle = "#232a3a"; ctx.lineWidth = 1;
|
|
335
|
+
modEdges.forEach(function (e) {
|
|
336
|
+
if (!modVisible(e.from) || !modVisible(e.to)) return;
|
|
337
|
+
var hot = impactSet && impactSet[e.from.id] && impactSet[e.to.id];
|
|
338
|
+
var a = toScreen(e.from.x, e.from.y), b = toScreen(e.to.x, e.to.y);
|
|
339
|
+
ctx.strokeStyle = hot ? "#f59e0b" : "#232a3a";
|
|
340
|
+
ctx.lineWidth = hot ? 1.8 : 1;
|
|
341
|
+
ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
|
|
342
|
+
});
|
|
343
|
+
modNodes.forEach(function (n) {
|
|
344
|
+
if (!modVisible(n)) return;
|
|
345
|
+
var p = toScreen(n.x, n.y);
|
|
346
|
+
var dim = impactSet && !impactSet[n.id];
|
|
347
|
+
var hot = impactSet && impactSet[n.id];
|
|
348
|
+
var r = n.kind === "unknown" ? 4 : 7;
|
|
349
|
+
ctx.globalAlpha = dim ? 0.15 : 1;
|
|
350
|
+
ctx.beginPath(); ctx.arc(p[0], p[1], selected === n.id ? r + 3 : r, 0, 7);
|
|
351
|
+
ctx.fillStyle = hot === 2 ? "#f59e0b" : KIND_COLORS[n.kind] || KIND_COLORS.unknown;
|
|
352
|
+
ctx.fill();
|
|
353
|
+
if (selected === n.id) { ctx.strokeStyle = "#e8edf5"; ctx.lineWidth = 2; ctx.stroke(); }
|
|
354
|
+
if (!dim) {
|
|
355
|
+
ctx.fillStyle = hot ? "#e8edf5" : "#9aa5b8";
|
|
356
|
+
ctx.font = "10px ui-monospace, monospace"; ctx.textAlign = "center";
|
|
357
|
+
ctx.fillText(n.id.split("/").pop(), p[0], p[1] - r - 5);
|
|
358
|
+
}
|
|
359
|
+
ctx.globalAlpha = 1;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function frame() {
|
|
364
|
+
tick();
|
|
365
|
+
if (view === "routes") drawRoutes(); else drawModules();
|
|
366
|
+
requestAnimationFrame(frame);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------- tabs ----------
|
|
370
|
+
function setView(v) {
|
|
371
|
+
view = v;
|
|
372
|
+
document.getElementById("tab-routes").className = "tab" + (v === "routes" ? " active" : "");
|
|
373
|
+
document.getElementById("tab-modules").className = "tab" + (v === "modules" ? " active" : "");
|
|
374
|
+
if (v === "routes") fitView(routeNodes.map(function (n) { return n.px; }), routeNodes.map(function (n) { return n.py; }));
|
|
375
|
+
else { alpha = Math.max(alpha, 0.4); fitView(modNodes.map(function (n) { return n.x; }), modNodes.map(function (n) { return n.y; })); }
|
|
376
|
+
}
|
|
377
|
+
document.getElementById("tab-routes").addEventListener("click", function () { setView("routes"); });
|
|
378
|
+
document.getElementById("tab-modules").addEventListener("click", function () { setView("modules"); });
|
|
379
|
+
|
|
380
|
+
// ---------- boot ----------
|
|
381
|
+
fetch("/graph.json").then(function (r) { return r.json(); }).then(function (g) {
|
|
382
|
+
graph = g;
|
|
383
|
+
buildModuleGraph();
|
|
384
|
+
buildRouteLayout();
|
|
385
|
+
alpha = 1;
|
|
386
|
+
// pre-simulate so the first paint is already organized
|
|
387
|
+
for (var i = 0; i < 300 && alpha > 0.05; i++) tick();
|
|
388
|
+
setView("routes");
|
|
389
|
+
frame();
|
|
390
|
+
});
|
|
391
|
+
<\/script>
|
|
392
|
+
</body>
|
|
393
|
+
</html>`;
|
|
394
|
+
async function f(a) {
|
|
395
|
+
const i = s.join(a, ".velojs", "graph.json"), o = r.createServer((n, e) => {
|
|
396
|
+
if (n.url === "/graph.json") {
|
|
397
|
+
let t;
|
|
398
|
+
try {
|
|
399
|
+
t = d.readFileSync(i, "utf-8");
|
|
400
|
+
} catch {
|
|
401
|
+
e.writeHead(404, { "Content-Type": "application/json" }), e.end(JSON.stringify({ error: "graph.json not found — run `velojs graph` first" }));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
e.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" }), e.end(t);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
e.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), e.end(l);
|
|
408
|
+
});
|
|
409
|
+
return await new Promise((n) => {
|
|
410
|
+
o.listen(0, "127.0.0.1", () => {
|
|
411
|
+
const e = o.address(), t = typeof e == "object" && e ? e.port : 0;
|
|
412
|
+
n(`http://127.0.0.1:${t}`);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
export {
|
|
417
|
+
f as serveGraphViewer
|
|
418
|
+
};
|
package/dist/graph.js
CHANGED
package/dist/vite.js
CHANGED