@mauroandre/velojs 0.0.38 → 0.0.40

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.
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: velojs-project-graph
3
+ description: "Use `.velojs/graph.json` to understand the project's route tree and module dependency graph before reading any source files. Always consult the graph first when exploring a VeloJS codebase. Generate it with `velojs graph` if missing."
4
+ ---
5
+
6
+ # Project Graph
7
+
8
+ VeloJS can generate a structural snapshot of your project — the route tree hierarchy, module dependency graph, and convention exports — as a single JSON file at `.velojs/graph.json`. This file gives an AI agent (or any tool) a complete map of the codebase in a few KB, replacing dozens of exploratory file reads.
9
+
10
+ ## When to use
11
+
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.
13
+
14
+ ## Generating the graph
15
+
16
+ If `.velojs/graph.json` does not exist, generate it:
17
+
18
+ ```bash
19
+ velojs graph
20
+ ```
21
+
22
+ This runs a fast Babel-based crawl of `app/routes.tsx` and its transitive imports. No build. No Vite. Sub-second. Works even if the project does not compile.
23
+
24
+ The graph is also regenerated automatically on every `velojs build` (via the `velo:graph` plugin) and on every file change during `velojs dev`.
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
+
55
+ ## Graph structure
56
+
57
+ ```typescript
58
+ interface GraphJson {
59
+ routes: RouteTreeNode[];
60
+ modules: Record<string, ModuleInfo>;
61
+ }
62
+
63
+ interface RouteTreeNode {
64
+ moduleId: string | null; // e.g. "pages/Home", null if no module
65
+ fullPath: string | null; // e.g. "/admin/users/:id"
66
+ path: string | null; // e.g. "/users/:id"
67
+ isRoot: boolean; // root layout (renders <html>)
68
+ middlewares: string[]; // middleware identifiers
69
+ children: RouteTreeNode[];
70
+ }
71
+
72
+ interface ModuleInfo {
73
+ file: string; // relative path from project root
74
+ kind: "root" | "layout" | "page" | "unknown";
75
+ exports: {
76
+ hasComponent: boolean;
77
+ hasLoader: boolean;
78
+ actions: string[]; // e.g. ["save", "delete"]
79
+ streams: string[]; // e.g. ["progress", "logs"]
80
+ sockets: string[]; // e.g. ["terminal"]
81
+ };
82
+ imports: string[]; // moduleIds this module depends on
83
+ importedBy: string[]; // moduleIds that depend on this module
84
+ }
85
+ ```
86
+
87
+ ## Common queries
88
+
89
+ ### "Which routes use this loader?"
90
+
91
+ 1. Find the loader's module in `modules` → note its `moduleId`
92
+ 2. Search `routes` recursively for nodes where `moduleId` matches
93
+ 3. The matching node's ancestors are the layouts wrapping it
94
+
95
+ ### "What breaks if I change this file?"
96
+
97
+ 1. Find the module in `modules[moduleId]`
98
+ 2. Follow `importedBy` — these modules import it
99
+ 3. Transitively follow `importedBy` on those modules
100
+ 4. Filter to modules where `kind` is `"page"` to see which pages are affected
101
+
102
+ ### "What actions does this page expose?"
103
+
104
+ 1. Find the page in `modules[moduleId]`
105
+ 2. Read `exports.actions` — e.g. `["save", "delete"]`
106
+ 3. These are the `action_*` exports callable from the client
107
+
108
+ ### "Where is the auth middleware applied?"
109
+
110
+ 1. Search `routes` recursively for nodes where `middlewares` includes the auth middleware name
111
+ 2. The matching nodes and their children are protected
112
+
113
+ ### "Which modules have no tests?"
114
+
115
+ Compare `modules` keys against the `tests/` directory. Modules with no corresponding test file are candidates for new tests.
116
+
117
+ ## Notes
118
+
119
+ - External imports (`@mauroandre/velojs`, `preact`, `wouter-preact`) are recorded in `imports` but not crawled
120
+ - `export function action_*` is NOT detected — only `export const action_*` (matches framework behavior)
121
+ - Path-less layouts (no `path` property) have `fullPath: ""` and `path: ""`
122
+ - The graph is regenerated on every build and on file changes during dev — never stale
package/dist/cli.js CHANGED
@@ -1,43 +1,44 @@
1
- import { spawnSync as n } from "node:child_process";
2
- import { resolve as l } from "node:path";
3
- const i = process.argv.slice(2), c = i[0], d = (e, t = []) => {
4
- const s = n(e, t, {
1
+ import { spawnSync as c } from "node:child_process";
2
+ import { join as n, resolve as u } from "node:path";
3
+ import p from "node:fs";
4
+ const o = process.argv.slice(2), a = o[0], v = (e, s = []) => {
5
+ const t = c(e, s, {
5
6
  stdio: "inherit",
6
7
  cwd: process.cwd()
7
8
  });
8
- process.exit(s.status ?? 0);
9
- }, p = () => {
10
- d("npx", ["vite", ...i.slice(1)]);
11
- }, u = (e) => {
12
- const t = Date.now().toString(36), s = {
9
+ process.exit(t.status ?? 0);
10
+ }, m = () => {
11
+ v("npx", ["vite", ...o.slice(1)]);
12
+ }, j = (e) => {
13
+ const s = Date.now().toString(36), t = {
13
14
  ...process.env,
14
- VELO_BUILD_HASH: t,
15
+ VELO_BUILD_HASH: s,
15
16
  ...e ? { VELO_STATIC: "1" } : {}
16
17
  };
17
18
  console.log("Building client...");
18
- const o = n("npx", ["vite", "build"], {
19
+ const r = c("npx", ["vite", "build"], {
19
20
  stdio: "inherit",
20
21
  cwd: process.cwd(),
21
- env: s
22
+ env: t
22
23
  });
23
- o.status !== 0 && (console.error("Client build failed. Aborting."), process.exit(o.status ?? 1)), console.log("Building server...");
24
- const r = n("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"], {
25
26
  stdio: "inherit",
26
27
  cwd: process.cwd(),
27
- env: s
28
+ env: t
28
29
  });
29
- r.status !== 0 && (console.error("Server build failed. Aborting."), process.exit(r.status ?? 1)), console.log("Build complete!"), e && v();
30
- }, v = 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 () => {
31
32
  process.env.NODE_ENV = "production", process.env.VELO_STATIC = "1";
32
- const t = await import(l(process.cwd(), "dist/server.js")), s = t.default, o = t.routes;
33
- (!s || !o) && (console.error("Failed to load app or routes from dist/server.js"), process.exit(1));
34
- const { generateStatic: r } = await import("./static.js");
35
- await r(s, o);
36
- }, m = () => {
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
+ const { generateStatic: i } = await import("./static.js");
36
+ await i(t, r);
37
+ }, f = () => {
37
38
  process.env.NODE_ENV = "production";
38
- const e = l(process.cwd(), "dist/server.js");
39
- d("node", [e]);
40
- }, a = () => {
39
+ const e = u(process.cwd(), "dist/server.js");
40
+ v("node", [e]);
41
+ }, d = () => {
41
42
  console.log(`
42
43
  VeloJS CLI
43
44
 
@@ -50,6 +51,8 @@ Commands:
50
51
  build Build for production (client + server)
51
52
  build --static Build as static site (HTML + JSON)
52
53
  start Start production server
54
+ graph [app-dir] Generate .velojs/graph.json (route tree + dependency graph)
55
+ graph --view Generate the graph and open an interactive viewer
53
56
 
54
57
  Examples:
55
58
  velojs init my-app
@@ -57,30 +60,43 @@ Examples:
57
60
  velojs build
58
61
  velojs build --static
59
62
  velojs start
63
+ velojs graph
64
+ velojs graph src/app
65
+ velojs graph --view
60
66
  `);
61
67
  };
62
- switch (c) {
68
+ switch (a) {
63
69
  case "init": {
64
70
  const { runInit: e } = await import("./init.js");
65
- await e(i[1]);
71
+ await e(o[1]);
66
72
  break;
67
73
  }
68
74
  case "dev":
69
- p();
75
+ m();
70
76
  break;
71
77
  case "build": {
72
- const e = i.includes("--static");
73
- u(e);
78
+ const e = o.includes("--static");
79
+ j(e);
74
80
  break;
75
81
  }
76
82
  case "start":
77
- m();
83
+ f();
84
+ break;
85
+ case "graph": {
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");
87
+ p.mkdirSync(l, { recursive: !0 });
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
+ }
78
93
  break;
94
+ }
79
95
  case "help":
80
96
  case "--help":
81
97
  case "-h":
82
- a();
98
+ d();
83
99
  break;
84
100
  default:
85
- c && console.error(`Unknown command: ${c}`), a(), process.exit(1);
101
+ a && console.error(`Unknown command: ${a}`), d(), process.exit(1);
86
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,378 @@
1
+ import r from "node:http";
2
+ import d from "node:fs";
3
+ import l from "node:path";
4
+ const s = `<!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 highlight · / = search</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 };
94
+ routeNodes.push(rn);
95
+ if (parent) 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 = 210, gapY = 110;
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
+ function tick() {
122
+ if (view !== "modules" || alpha < 0.005) return;
123
+ alpha *= 0.995;
124
+ var i, j, n, m, dx, dy, d2, f;
125
+ for (i = 0; i < modNodes.length; i++) {
126
+ n = modNodes[i];
127
+ for (j = i + 1; j < modNodes.length; j++) {
128
+ m = modNodes[j];
129
+ dx = n.x - m.x; dy = n.y - m.y;
130
+ d2 = dx * dx + dy * dy + 0.01;
131
+ f = 2600 / d2 * alpha;
132
+ var d = Math.sqrt(d2);
133
+ n.vx += dx / d * f; n.vy += dy / d * f;
134
+ m.vx -= dx / d * f; m.vy -= dy / d * f;
135
+ }
136
+ }
137
+ modEdges.forEach(function (e) {
138
+ dx = e.to.x - e.from.x; dy = e.to.y - e.from.y;
139
+ var d = Math.sqrt(dx * dx + dy * dy) || 1;
140
+ f = (d - 110) * 0.02 * alpha;
141
+ e.from.vx += dx / d * f * d * 0.02; e.from.vy += dy / d * f * d * 0.02;
142
+ e.to.vx -= dx / d * f * d * 0.02; e.to.vy -= dy / d * f * d * 0.02;
143
+ });
144
+ modNodes.forEach(function (n) {
145
+ if (n === dragNode) return;
146
+ n.vx += -n.x * 0.002 * alpha; n.vy += -n.y * 0.002 * alpha;
147
+ n.vx *= 0.85; n.vy *= 0.85;
148
+ n.x += n.vx; n.y += n.vy;
149
+ });
150
+ }
151
+
152
+ // ---------- impact highlight ----------
153
+ var hoverNode = null;
154
+ var impactSet = null;
155
+ function computeImpact(n) {
156
+ var set = {}, queue = [n.id];
157
+ set[n.id] = 2;
158
+ while (queue.length) {
159
+ var id = queue.shift();
160
+ var mod = graph.modules[id];
161
+ if (!mod) continue;
162
+ mod.importedBy.forEach(function (up) {
163
+ if (!(up in set)) { set[up] = 1; queue.push(up); }
164
+ });
165
+ }
166
+ return set;
167
+ }
168
+
169
+ // ---------- selection / panel ----------
170
+ var selected = null;
171
+ var panel = document.getElementById("panel");
172
+ function showPanel(id) {
173
+ selected = id;
174
+ var m = graph.modules[id];
175
+ if (!m) { panel.className = ""; panel.innerHTML = ""; return; }
176
+ var b = [];
177
+ if (m.exports.hasComponent) b.push('<span class="badge">Component</span>');
178
+ if (m.exports.hasLoader) b.push('<span class="badge">loader</span>');
179
+ m.exports.actions.forEach(function (a) { b.push('<span class="badge">action_' + a + "</span>"); });
180
+ m.exports.streams.forEach(function (s) { b.push('<span class="badge">stream_' + s + "</span>"); });
181
+ m.exports.sockets.forEach(function (s) { b.push('<span class="badge">socket_' + s + "</span>"); });
182
+ var html = "<h2>" + id + '</h2><div class="file">' + m.file + " · " + m.kind + "</div><div>" + b.join("") + "</div>";
183
+ html += "<h3>imports (" + m.imports.length + ")</h3><ul>";
184
+ m.imports.forEach(function (i) {
185
+ html += modById[i] ? "<li data-id='" + i + "'>→ " + i + "</li>" : "<li class='ext'>→ " + i + "</li>";
186
+ });
187
+ html += "</ul><h3>imported by (" + m.importedBy.length + ")</h3><ul>";
188
+ m.importedBy.forEach(function (i) { html += "<li data-id='" + i + "'>← " + i + "</li>"; });
189
+ html += "</ul>";
190
+ panel.innerHTML = html;
191
+ panel.className = "open";
192
+ panel.querySelectorAll("li[data-id]").forEach(function (li) {
193
+ li.addEventListener("click", function () { showPanel(li.getAttribute("data-id")); focusModule(li.getAttribute("data-id")); });
194
+ });
195
+ }
196
+ function focusModule(id) {
197
+ var n = modById[id];
198
+ if (!n) return;
199
+ if (view !== "modules") setView("modules");
200
+ cam.x = W / 2 - n.x * cam.k; cam.y = H / 2 - n.y * cam.k;
201
+ }
202
+
203
+ // ---------- search ----------
204
+ var query = "";
205
+ document.getElementById("search").addEventListener("input", function (e) { query = e.target.value.toLowerCase(); });
206
+ document.addEventListener("keydown", function (e) {
207
+ if (e.key === "/" && document.activeElement !== document.getElementById("search")) { e.preventDefault(); document.getElementById("search").focus(); }
208
+ if (e.key === "Escape") { panel.className = ""; selected = null; query = ""; document.getElementById("search").value = ""; }
209
+ });
210
+
211
+ // ---------- interaction ----------
212
+ var dragNode = null, panning = false, lastX = 0, lastY = 0, moved = false;
213
+ function nodeAt(sx, sy) {
214
+ var w = toWorld(sx, sy);
215
+ var list = view === "modules" ? modNodes : routeNodes;
216
+ for (var i = list.length - 1; i >= 0; i--) {
217
+ var n = list[i];
218
+ var nx = view === "modules" ? n.x : n.px, ny = view === "modules" ? n.y : n.py;
219
+ var dx = w[0] - nx, dy = w[1] - ny;
220
+ if (dx * dx + dy * dy < 14 * 14 / (cam.k * cam.k) + 36) return n;
221
+ }
222
+ return null;
223
+ }
224
+ canvas.addEventListener("mousedown", function (e) {
225
+ var n = nodeAt(e.clientX, e.clientY);
226
+ if (n && view === "modules") { dragNode = n; alpha = Math.max(alpha, 0.3); }
227
+ else panning = true;
228
+ lastX = e.clientX; lastY = e.clientY; moved = false;
229
+ });
230
+ canvas.addEventListener("mousemove", function (e) {
231
+ var dx = e.clientX - lastX, dy = e.clientY - lastY;
232
+ if (Math.abs(dx) + Math.abs(dy) > 3) moved = true;
233
+ if (dragNode) { var w = toWorld(e.clientX, e.clientY); dragNode.x = w[0]; dragNode.y = w[1]; dragNode.vx = dragNode.vy = 0; }
234
+ else if (panning) { cam.x += dx; cam.y += dy; }
235
+ else {
236
+ var n = nodeAt(e.clientX, e.clientY);
237
+ hoverNode = view === "modules" ? n : null;
238
+ impactSet = hoverNode ? computeImpact(hoverNode) : null;
239
+ canvas.style.cursor = n ? "pointer" : "grab";
240
+ }
241
+ lastX = e.clientX; lastY = e.clientY;
242
+ });
243
+ canvas.addEventListener("mouseup", function (e) {
244
+ if (!moved) {
245
+ var n = nodeAt(e.clientX, e.clientY);
246
+ if (view === "modules") showPanel(n ? n.id : null);
247
+ else if (n && n.data.moduleId) showPanel(n.data.moduleId);
248
+ else showPanel(null);
249
+ }
250
+ dragNode = null; panning = false;
251
+ });
252
+ canvas.addEventListener("wheel", function (e) {
253
+ e.preventDefault();
254
+ var k = Math.exp(-e.deltaY * 0.0012);
255
+ var w = toWorld(e.clientX, e.clientY);
256
+ cam.k *= k;
257
+ cam.x = e.clientX - w[0] * cam.k; cam.y = e.clientY - w[1] * cam.k;
258
+ }, { passive: false });
259
+
260
+ // ---------- rendering ----------
261
+ function matches(id) { return !query || id.toLowerCase().indexOf(query) >= 0; }
262
+
263
+ function drawRoutes() {
264
+ ctx.clearRect(0, 0, W, H);
265
+ ctx.strokeStyle = "#2a3247"; ctx.lineWidth = 1.2;
266
+ routeEdges.forEach(function (e) {
267
+ var a = toScreen(e.from.px, e.from.py), b = toScreen(e.to.px, e.to.py);
268
+ ctx.beginPath();
269
+ ctx.moveTo(a[0], a[1]);
270
+ ctx.bezierCurveTo(a[0], (a[1] + b[1]) / 2, b[0], (a[1] + b[1]) / 2, b[0], b[1]);
271
+ ctx.stroke();
272
+ });
273
+ routeNodes.forEach(function (n) {
274
+ var d = n.data;
275
+ var id = d.moduleId || "(wrapper)";
276
+ if (!matches(id)) return;
277
+ var p = toScreen(n.px, n.py);
278
+ var color = d.isRoot ? KIND_COLORS.root : (d.children.length ? KIND_COLORS.layout : KIND_COLORS.page);
279
+ var sel = selected === d.moduleId;
280
+ ctx.beginPath(); ctx.arc(p[0], p[1], sel ? 9 : 6, 0, 7);
281
+ ctx.fillStyle = color; ctx.fill();
282
+ if (sel) { ctx.strokeStyle = "#e8edf5"; ctx.lineWidth = 2; ctx.stroke(); }
283
+ ctx.fillStyle = "#d7dde8"; ctx.font = "11px ui-monospace, monospace"; ctx.textAlign = "center";
284
+ ctx.fillText(id.split("/").pop(), p[0], p[1] - 13);
285
+ ctx.fillStyle = "#55607a"; ctx.font = "10px ui-monospace, monospace";
286
+ var sub = d.fullPath || "";
287
+ if (d.middlewares.length) sub += " · " + d.middlewares.join(", ");
288
+ ctx.fillText(sub, p[0], p[1] + 20);
289
+ });
290
+ }
291
+
292
+ function drawModules() {
293
+ ctx.clearRect(0, 0, W, H);
294
+ ctx.strokeStyle = "#232a3a"; ctx.lineWidth = 1;
295
+ modEdges.forEach(function (e) {
296
+ if (!matches(e.from.id) && !matches(e.to.id)) return;
297
+ var hot = impactSet && impactSet[e.from.id] && impactSet[e.to.id];
298
+ var a = toScreen(e.from.x, e.from.y), b = toScreen(e.to.x, e.to.y);
299
+ ctx.strokeStyle = hot ? "#f59e0b" : "#232a3a";
300
+ ctx.lineWidth = hot ? 1.8 : 1;
301
+ ctx.beginPath(); ctx.moveTo(a[0], a[1]); ctx.lineTo(b[0], b[1]); ctx.stroke();
302
+ });
303
+ modNodes.forEach(function (n) {
304
+ if (!matches(n.id)) return;
305
+ var p = toScreen(n.x, n.y);
306
+ var dim = impactSet && !impactSet[n.id];
307
+ var hot = impactSet && impactSet[n.id];
308
+ var r = n.kind === "unknown" ? 4 : 7;
309
+ ctx.globalAlpha = dim ? 0.15 : 1;
310
+ ctx.beginPath(); ctx.arc(p[0], p[1], selected === n.id ? r + 3 : r, 0, 7);
311
+ ctx.fillStyle = hot === 2 ? "#f59e0b" : KIND_COLORS[n.kind] || KIND_COLORS.unknown;
312
+ ctx.fill();
313
+ if (selected === n.id) { ctx.strokeStyle = "#e8edf5"; ctx.lineWidth = 2; ctx.stroke(); }
314
+ if (!dim) {
315
+ ctx.fillStyle = hot ? "#e8edf5" : "#9aa5b8";
316
+ ctx.font = "10px ui-monospace, monospace"; ctx.textAlign = "center";
317
+ ctx.fillText(n.id.split("/").pop(), p[0], p[1] - r - 5);
318
+ }
319
+ ctx.globalAlpha = 1;
320
+ });
321
+ }
322
+
323
+ function frame() {
324
+ tick();
325
+ if (view === "routes") drawRoutes(); else drawModules();
326
+ requestAnimationFrame(frame);
327
+ }
328
+
329
+ // ---------- tabs ----------
330
+ function setView(v) {
331
+ view = v;
332
+ document.getElementById("tab-routes").className = "tab" + (v === "routes" ? " active" : "");
333
+ document.getElementById("tab-modules").className = "tab" + (v === "modules" ? " active" : "");
334
+ if (v === "routes") fitView(routeNodes.map(function (n) { return n.px; }), routeNodes.map(function (n) { return n.py; }));
335
+ else { alpha = Math.max(alpha, 0.4); fitView(modNodes.map(function (n) { return n.x; }), modNodes.map(function (n) { return n.y; })); }
336
+ }
337
+ document.getElementById("tab-routes").addEventListener("click", function () { setView("routes"); });
338
+ document.getElementById("tab-modules").addEventListener("click", function () { setView("modules"); });
339
+
340
+ // ---------- boot ----------
341
+ fetch("/graph.json").then(function (r) { return r.json(); }).then(function (g) {
342
+ graph = g;
343
+ buildModuleGraph();
344
+ buildRouteLayout();
345
+ alpha = 1;
346
+ // pre-simulate so the first paint is already organized
347
+ for (var i = 0; i < 300 && alpha > 0.05; i++) tick();
348
+ setView("routes");
349
+ frame();
350
+ });
351
+ <\/script>
352
+ </body>
353
+ </html>`;
354
+ async function f(o) {
355
+ const i = l.join(o, ".velojs", "graph.json"), a = r.createServer((n, e) => {
356
+ if (n.url === "/graph.json") {
357
+ let t;
358
+ try {
359
+ t = d.readFileSync(i, "utf-8");
360
+ } catch {
361
+ e.writeHead(404, { "Content-Type": "application/json" }), e.end(JSON.stringify({ error: "graph.json not found — run `velojs graph` first" }));
362
+ return;
363
+ }
364
+ e.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" }), e.end(t);
365
+ return;
366
+ }
367
+ e.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }), e.end(s);
368
+ });
369
+ return await new Promise((n) => {
370
+ a.listen(0, "127.0.0.1", () => {
371
+ const e = a.address(), t = typeof e == "object" && e ? e.port : 0;
372
+ n(`http://127.0.0.1:${t}`);
373
+ });
374
+ });
375
+ }
376
+ export {
377
+ f as serveGraphViewer
378
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * VeloJS Project Graph
3
+ *
4
+ * Builds a structural JSON snapshot of the project — route tree hierarchy,
5
+ * module dependency graph, and convention exports — from source code alone.
6
+ * No Vite, no dev server, no build. Used by `velojs graph` and by the
7
+ * `velo:graph` Vite plugin.
8
+ */
9
+ export interface RouteTreeNode {
10
+ moduleId: string | null;
11
+ fullPath: string | null;
12
+ path: string | null;
13
+ isRoot: boolean;
14
+ middlewares: string[];
15
+ children: RouteTreeNode[];
16
+ }
17
+ export interface ModuleExports {
18
+ hasComponent: boolean;
19
+ hasLoader: boolean;
20
+ actions: string[];
21
+ streams: string[];
22
+ sockets: string[];
23
+ }
24
+ export interface ModuleInfo {
25
+ file: string;
26
+ kind: "root" | "layout" | "page" | "unknown";
27
+ exports: ModuleExports;
28
+ imports: string[];
29
+ importedBy: string[];
30
+ }
31
+ export interface GraphJson {
32
+ routes: RouteTreeNode[];
33
+ modules: Record<string, ModuleInfo>;
34
+ }
35
+ export declare function detectExports(filePath: string): ModuleExports;
36
+ export declare function buildRouteTree(code: string): RouteTreeNode[];
37
+ export declare function buildGraph(appDir: string): GraphJson;