@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.
@@ -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, check `.velojs/graph.json` first. It tells you:
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 a } from "node:child_process";
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 r = process.argv.slice(2), c = r[0], v = (e, s = []) => {
5
- const t = a(e, s, {
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
- }, g = () => {
11
- v("npx", ["vite", ...r.slice(1)]);
12
- }, h = (e) => {
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 o = a("npx", ["vite", "build"], {
19
+ const r = c("npx", ["vite", "build"], {
20
20
  stdio: "inherit",
21
21
  cwd: process.cwd(),
22
22
  env: t
23
23
  });
24
- o.status !== 0 && (console.error("Client build failed. Aborting."), process.exit(o.status ?? 1)), console.log("Building server...");
25
- const i = a("npx", ["vite", "build", "--mode", "server"], {
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 && w();
31
- }, w = async () => {
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, o = s.routes;
34
- (!t || !o) && (console.error("Failed to load app or routes from dist/server.js"), process.exit(1));
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, o);
37
- }, b = () => {
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 (c) {
68
+ switch (a) {
67
69
  case "init": {
68
70
  const { runInit: e } = await import("./init.js");
69
- await e(r[1]);
71
+ await e(o[1]);
70
72
  break;
71
73
  }
72
74
  case "dev":
73
- g();
75
+ m();
74
76
  break;
75
77
  case "build": {
76
- const e = r.includes("--static");
77
- h(e);
78
+ const e = o.includes("--static");
79
+ j(e);
78
80
  break;
79
81
  }
80
82
  case "start":
81
- b();
83
+ f();
82
84
  break;
83
85
  case "graph": {
84
- const { buildGraph: e } = await import("./graph.js"), s = process.cwd(), t = r[1] && !r[1].startsWith("-") ? r[1] : null, o = n(s, t ?? "app"), i = e(o), l = n(s, ".velojs");
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 m = n(l, "graph.json");
87
- p.writeFileSync(m, JSON.stringify(i, null, 2)), console.log("Graph written to .velojs/graph.json");
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
- c && console.error(`Unknown command: ${c}`), d(), process.exit(1);
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
@@ -149,7 +149,7 @@ function B(i, e) {
149
149
  } else
150
150
  r.push(m);
151
151
  }
152
- }), s.set(c, { file: t, imports: r });
152
+ }), s.set(c, { file: t, imports: [...new Set(r)] });
153
153
  }
154
154
  return u(i), { modules: s };
155
155
  }
package/dist/vite.js CHANGED
@@ -602,7 +602,7 @@ function ne(p, o) {
602
602
  const c = U(s), r = d.join(n, ".velojs");
603
603
  g.mkdirSync(r, { recursive: !0 }), g.writeFileSync(
604
604
  d.join(r, "graph.json"),
605
- JSON.stringify(c, null, 2)
605
+ JSON.stringify(c)
606
606
  );
607
607
  }
608
608
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mauroandre/velojs",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "type": "module",
5
5
  "description": "Fullstack web framework built on Hono + Preact with Vite-powered AST transforms",
6
6
  "keywords": [