@mauroandre/velojs 0.0.38 → 0.0.39

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,97 @@
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, 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_*`
17
+
18
+ ## Generating the graph
19
+
20
+ If `.velojs/graph.json` does not exist, generate it:
21
+
22
+ ```bash
23
+ velojs graph
24
+ ```
25
+
26
+ 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.
27
+
28
+ The graph is also regenerated automatically on every `velojs build` (via the `velo:graph` plugin) and on every file change during `velojs dev`.
29
+
30
+ ## Graph structure
31
+
32
+ ```typescript
33
+ interface GraphJson {
34
+ routes: RouteTreeNode[];
35
+ modules: Record<string, ModuleInfo>;
36
+ }
37
+
38
+ interface RouteTreeNode {
39
+ moduleId: string | null; // e.g. "pages/Home", null if no module
40
+ fullPath: string | null; // e.g. "/admin/users/:id"
41
+ path: string | null; // e.g. "/users/:id"
42
+ isRoot: boolean; // root layout (renders <html>)
43
+ middlewares: string[]; // middleware identifiers
44
+ children: RouteTreeNode[];
45
+ }
46
+
47
+ interface ModuleInfo {
48
+ file: string; // relative path from project root
49
+ kind: "root" | "layout" | "page" | "unknown";
50
+ exports: {
51
+ hasComponent: boolean;
52
+ hasLoader: boolean;
53
+ actions: string[]; // e.g. ["save", "delete"]
54
+ streams: string[]; // e.g. ["progress", "logs"]
55
+ sockets: string[]; // e.g. ["terminal"]
56
+ };
57
+ imports: string[]; // moduleIds this module depends on
58
+ importedBy: string[]; // moduleIds that depend on this module
59
+ }
60
+ ```
61
+
62
+ ## Common queries
63
+
64
+ ### "Which routes use this loader?"
65
+
66
+ 1. Find the loader's module in `modules` → note its `moduleId`
67
+ 2. Search `routes` recursively for nodes where `moduleId` matches
68
+ 3. The matching node's ancestors are the layouts wrapping it
69
+
70
+ ### "What breaks if I change this file?"
71
+
72
+ 1. Find the module in `modules[moduleId]`
73
+ 2. Follow `importedBy` — these modules import it
74
+ 3. Transitively follow `importedBy` on those modules
75
+ 4. Filter to modules where `kind` is `"page"` to see which pages are affected
76
+
77
+ ### "What actions does this page expose?"
78
+
79
+ 1. Find the page in `modules[moduleId]`
80
+ 2. Read `exports.actions` — e.g. `["save", "delete"]`
81
+ 3. These are the `action_*` exports callable from the client
82
+
83
+ ### "Where is the auth middleware applied?"
84
+
85
+ 1. Search `routes` recursively for nodes where `middlewares` includes the auth middleware name
86
+ 2. The matching nodes and their children are protected
87
+
88
+ ### "Which modules have no tests?"
89
+
90
+ Compare `modules` keys against the `tests/` directory. Modules with no corresponding test file are candidates for new tests.
91
+
92
+ ## Notes
93
+
94
+ - External imports (`@mauroandre/velojs`, `preact`, `wouter-preact`) are recorded in `imports` but not crawled
95
+ - `export function action_*` is NOT detected — only `export const action_*` (matches framework behavior)
96
+ - Path-less layouts (no `path` property) have `fullPath: ""` and `path: ""`
97
+ - 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 a } from "node:child_process";
2
+ import { join as n, resolve as u } from "node:path";
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, {
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
+ }, g = () => {
11
+ v("npx", ["vite", ...r.slice(1)]);
12
+ }, h = (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 o = a("npx", ["vite", "build"], {
19
20
  stdio: "inherit",
20
21
  cwd: process.cwd(),
21
- env: s
22
+ env: t
22
23
  });
23
24
  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"], {
25
+ const i = a("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 && w();
31
+ }, w = 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, o = s.routes;
34
+ (!t || !o) && (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, o);
37
+ }, b = () => {
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,7 @@ 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)
53
55
 
54
56
  Examples:
55
57
  velojs init my-app
@@ -57,30 +59,39 @@ Examples:
57
59
  velojs build
58
60
  velojs build --static
59
61
  velojs start
62
+ velojs graph
63
+ velojs graph src/app
60
64
  `);
61
65
  };
62
66
  switch (c) {
63
67
  case "init": {
64
68
  const { runInit: e } = await import("./init.js");
65
- await e(i[1]);
69
+ await e(r[1]);
66
70
  break;
67
71
  }
68
72
  case "dev":
69
- p();
73
+ g();
70
74
  break;
71
75
  case "build": {
72
- const e = i.includes("--static");
73
- u(e);
76
+ const e = r.includes("--static");
77
+ h(e);
74
78
  break;
75
79
  }
76
80
  case "start":
77
- m();
81
+ b();
78
82
  break;
83
+ 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");
85
+ 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
+ break;
89
+ }
79
90
  case "help":
80
91
  case "--help":
81
92
  case "-h":
82
- a();
93
+ d();
83
94
  break;
84
95
  default:
85
- c && console.error(`Unknown command: ${c}`), a(), process.exit(1);
96
+ c && console.error(`Unknown command: ${c}`), d(), process.exit(1);
86
97
  }
@@ -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;
package/dist/graph.js ADDED
@@ -0,0 +1,202 @@
1
+ import y from "node:fs";
2
+ import E from "node:path";
3
+ import { parse as T } from "@babel/parser";
4
+ import w from "@babel/traverse";
5
+ import * as a from "@babel/types";
6
+ const g = typeof w == "function" ? w : w.default, W = /export\s+(const|function)\s+Component/, _ = /export\s+(const|function)\s+loader/, b = /export\s+const\s+action_(\w+)/g, k = /export\s+const\s+stream_(\w+)/g, M = /export\s+const\s+socket_(\w+)/g;
7
+ function A(i) {
8
+ let e;
9
+ try {
10
+ e = y.readFileSync(i, "utf-8");
11
+ } catch {
12
+ return {
13
+ hasComponent: !1,
14
+ hasLoader: !1,
15
+ actions: [],
16
+ streams: [],
17
+ sockets: []
18
+ };
19
+ }
20
+ const s = [];
21
+ let o;
22
+ const u = new RegExp(b.source, "g");
23
+ for (; (o = u.exec(e)) !== null; )
24
+ s.push(o[1]);
25
+ const t = [], c = new RegExp(k.source, "g");
26
+ for (; (o = c.exec(e)) !== null; )
27
+ t.push(o[1]);
28
+ const n = [], r = new RegExp(M.source, "g");
29
+ for (; (o = r.exec(e)) !== null; )
30
+ n.push(o[1]);
31
+ return {
32
+ hasComponent: W.test(e),
33
+ hasLoader: _.test(e),
34
+ actions: s,
35
+ streams: t,
36
+ sockets: n
37
+ };
38
+ }
39
+ function C(i) {
40
+ const e = T(i, {
41
+ sourceType: "module",
42
+ plugins: ["typescript", "jsx"]
43
+ }), s = /* @__PURE__ */ new Map();
44
+ g(e, {
45
+ ImportDeclaration(u) {
46
+ const t = u.node.source.value;
47
+ for (const c of u.node.specifiers)
48
+ a.isImportNamespaceSpecifier(c) && s.set(c.local.name, t);
49
+ }
50
+ });
51
+ let o = null;
52
+ return g(e, {
53
+ ExportDefaultDeclaration(u) {
54
+ const t = u.node.declaration;
55
+ a.isArrayExpression(t) ? o = t : (a.isTSSatisfiesExpression(t) || a.isTSAsExpression(t)) && a.isArrayExpression(t.expression) && (o = t.expression);
56
+ }
57
+ }), { imports: s, arrayNode: o };
58
+ }
59
+ function L(i, e) {
60
+ if (!i) return null;
61
+ const s = e.get(i);
62
+ return s ? s.replace(/^\.\//, "").replace(/\.(tsx?|jsx?|js)$/, "") : null;
63
+ }
64
+ function O(i, e, s) {
65
+ const o = [];
66
+ for (const u of i) {
67
+ if (!a.isObjectExpression(u)) continue;
68
+ let t = "", c = e, n = null, r = null, d = !1;
69
+ const x = [], m = u.properties.filter(
70
+ (l) => a.isObjectProperty(l)
71
+ );
72
+ for (const l of m) {
73
+ const j = l.key, I = a.isIdentifier(j) ? j.name : null;
74
+ if (I === "path" && a.isStringLiteral(l.value) && (t = l.value.value, t && !t.startsWith("/") ? c = e + "/" + t : c = e + t), I === "module" && a.isIdentifier(l.value) && (n = l.value.name), I === "children" && a.isArrayExpression(l.value) && (r = l.value), I === "isRoot" && a.isBooleanLiteral(l.value) && (d = l.value.value), I === "middlewares" && a.isArrayExpression(l.value))
75
+ for (const R of l.value.elements)
76
+ R && a.isIdentifier(R) && x.push(R.name);
77
+ }
78
+ const f = L(n, s), h = !r && t === "/" ? e || "/" : c, p = r ? O(r.elements, c, s) : [];
79
+ o.push({
80
+ moduleId: f,
81
+ fullPath: f ? h : null,
82
+ path: f ? t : null,
83
+ isRoot: d,
84
+ middlewares: x,
85
+ children: p
86
+ });
87
+ }
88
+ return o;
89
+ }
90
+ function D(i) {
91
+ const { imports: e, arrayNode: s } = C(i);
92
+ return s ? O(s.elements, "", e) : [];
93
+ }
94
+ const S = [".tsx", ".ts", ".jsx", ".js"];
95
+ function F(i, e, s) {
96
+ if (e.startsWith(".") && (e.endsWith(".js") || e.endsWith(".jsx") || e.endsWith(".ts") || e.endsWith(".tsx"))) {
97
+ const o = E.dirname(i), t = E.resolve(o, e).replace(/\.(tsx?|jsx?|js)$/, "");
98
+ for (const n of S) {
99
+ const r = t + n;
100
+ if (y.existsSync(r)) return r;
101
+ }
102
+ const c = [t];
103
+ for (const n of c)
104
+ if (y.existsSync(n) && y.statSync(n).isDirectory())
105
+ for (const r of S) {
106
+ const d = E.join(n, "index" + r);
107
+ if (y.existsSync(d)) return d;
108
+ }
109
+ return null;
110
+ }
111
+ return null;
112
+ }
113
+ function N(i, e) {
114
+ const s = E.relative(e, i);
115
+ return s.startsWith("..") ? null : s.replace(/\.(tsx?|jsx?|js)$/, "");
116
+ }
117
+ function B(i, e) {
118
+ const s = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Set();
119
+ function u(t) {
120
+ if (o.has(t)) return;
121
+ o.add(t);
122
+ const c = N(t, e);
123
+ if (!c) return;
124
+ let n;
125
+ try {
126
+ n = y.readFileSync(t, "utf-8");
127
+ } catch {
128
+ return;
129
+ }
130
+ const r = [], d = T(n, {
131
+ sourceType: "module",
132
+ plugins: ["typescript", "jsx"]
133
+ });
134
+ g(d, {
135
+ ImportDeclaration(x) {
136
+ const m = x.node.source.value;
137
+ if (m.startsWith(".")) {
138
+ const f = F(
139
+ t,
140
+ m
141
+ );
142
+ if (f) {
143
+ const v = N(
144
+ f,
145
+ e
146
+ );
147
+ v && (r.push(v), u(f));
148
+ }
149
+ } else
150
+ r.push(m);
151
+ }
152
+ }), s.set(c, { file: t, imports: r });
153
+ }
154
+ return u(i), { modules: s };
155
+ }
156
+ function q(i) {
157
+ const e = E.join(i, "routes.tsx");
158
+ let s;
159
+ try {
160
+ s = y.readFileSync(e, "utf-8");
161
+ } catch {
162
+ return { routes: [], modules: {} };
163
+ }
164
+ const o = D(s), { modules: u } = B(
165
+ e,
166
+ i
167
+ ), t = {}, c = /* @__PURE__ */ new Map();
168
+ for (const [n, r] of u) {
169
+ let d = "unknown";
170
+ const x = (h) => {
171
+ for (const p of h) {
172
+ if (p.moduleId === n)
173
+ return p.isRoot ? "root" : p.children.length > 0 ? "layout" : "page";
174
+ const l = x(p.children);
175
+ if (l) return l;
176
+ }
177
+ return null;
178
+ }, m = x(o);
179
+ m && (d = m);
180
+ const f = r.file, v = A(f);
181
+ for (const h of r.imports) {
182
+ c.has(h) || c.set(h, []);
183
+ const p = c.get(h);
184
+ p.includes(n) || p.push(n);
185
+ }
186
+ t[n] = {
187
+ file: E.relative(process.cwd(), f),
188
+ kind: d,
189
+ exports: v,
190
+ imports: r.imports,
191
+ importedBy: []
192
+ };
193
+ }
194
+ for (const [n, r] of Object.entries(t))
195
+ r.importedBy = c.get(n) ?? [];
196
+ return { routes: o, modules: t };
197
+ }
198
+ export {
199
+ q as buildGraph,
200
+ D as buildRouteTree,
201
+ A as detectExports
202
+ };
package/dist/init.js CHANGED
@@ -7,6 +7,15 @@ function m() {
7
7
  return JSON.parse(t.readFileSync(e, "utf-8")).version;
8
8
  }
9
9
  const u = {
10
+ ".gitignore": `# Dependencies
11
+ node_modules/
12
+
13
+ # Build output
14
+ dist/
15
+
16
+ # Project graph
17
+ .velojs/
18
+ `,
10
19
  "package.json": `{
11
20
  "name": "my-velojs-app",
12
21
  "version": "0.1.0",
@@ -139,22 +148,22 @@ a:hover {
139
148
  `
140
149
  };
141
150
  async function x(o) {
142
- const e = o ? s.resolve(process.cwd(), o) : process.cwd(), r = s.basename(e);
151
+ const e = o ? s.resolve(process.cwd(), o) : process.cwd(), n = s.basename(e);
143
152
  t.existsSync(e) ? t.readdirSync(e).filter(
144
- (n) => !n.startsWith(".") && n !== "node_modules"
153
+ (r) => !r.startsWith(".") && r !== "node_modules"
145
154
  ).length > 0 && (console.error(
146
- `Error: Directory "${r}" is not empty. Use an empty directory.`
155
+ `Error: Directory "${n}" is not empty. Use an empty directory.`
147
156
  ), process.exit(1)) : t.mkdirSync(e, { recursive: !0 });
148
- for (const [a, n] of Object.entries(u)) {
157
+ for (const [a, r] of Object.entries(u)) {
149
158
  const p = s.join(e, a), l = s.dirname(p);
150
159
  t.existsSync(l) || t.mkdirSync(l, { recursive: !0 });
151
- const c = a === "package.json" ? n.replace("my-velojs-app", r) : n;
160
+ const c = a === "package.json" ? r.replace("my-velojs-app", n) : r;
152
161
  t.writeFileSync(p, c);
153
162
  }
154
163
  console.log(`
155
- Project created in ${o ? r : "current directory"}!
164
+ Project created in ${o ? n : "current directory"}!
156
165
  `), console.log(`Next steps:
157
- `), o && console.log(` cd ${r}`), console.log(" npm install"), console.log(` npx velojs dev
166
+ `), o && console.log(` cd ${n}`), console.log(" npm install"), console.log(` npx velojs dev
158
167
  `);
159
168
  }
160
169
  export {