@mauroandre/velojs 0.0.36 → 0.0.38

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.
Files changed (32) hide show
  1. package/README.md +20 -10
  2. package/agent/AGENTS.md +143 -0
  3. package/agent/skills/velojs-actions/SKILL.md +192 -0
  4. package/agent/skills/velojs-components/SKILL.md +99 -0
  5. package/agent/skills/velojs-endpoints/SKILL.md +187 -0
  6. package/agent/skills/velojs-event-streams/SKILL.md +705 -0
  7. package/agent/skills/velojs-getting-started/SKILL.md +191 -0
  8. package/agent/skills/velojs-hooks/SKILL.md +103 -0
  9. package/agent/skills/velojs-link-component/SKILL.md +67 -0
  10. package/agent/skills/velojs-loaders/SKILL.md +134 -0
  11. package/agent/skills/velojs-middlewares/SKILL.md +143 -0
  12. package/agent/skills/velojs-production-deploy/SKILL.md +96 -0
  13. package/agent/skills/velojs-routes/SKILL.md +209 -0
  14. package/agent/skills/velojs-scripts-component/SKILL.md +62 -0
  15. package/agent/skills/velojs-server-api/SKILL.md +155 -0
  16. package/agent/skills/velojs-sockets/SKILL.md +303 -0
  17. package/agent/skills/velojs-static-generation/SKILL.md +148 -0
  18. package/agent/skills/velojs-testing/SKILL.md +472 -0
  19. package/agent/skills/velojs-type-reference/SKILL.md +142 -0
  20. package/agent/skills/velojs-vite-plugin/SKILL.md +71 -0
  21. package/dist/client.js +58 -47
  22. package/dist/components.js +18 -16
  23. package/dist/hooks.d.ts +3 -2
  24. package/dist/hooks.js +107 -110
  25. package/dist/loader-store.d.ts +49 -0
  26. package/dist/loader-store.js +103 -0
  27. package/dist/server.js +11 -11
  28. package/dist/testing/createTestApp.d.ts +1 -1
  29. package/dist/testing/index.d.ts +1 -1
  30. package/dist/testing/types.d.ts +7 -3
  31. package/dist/vite.js +276 -277
  32. package/package.json +6 -2
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: velojs-production-deploy
3
+ description: "Deploying to production: `velojs build`/`start`, Docker, env vars (`PORT`, `NODE_ENV`, `STATIC_BASE_URL`), serving assets from a CDN. Use when writing a Dockerfile, configuring a host, or shipping to production."
4
+ ---
5
+
6
+ # Production Deploy
7
+
8
+ VeloJS apps compile to a simple structure: a `dist/` folder with a server entry and static assets. This makes deployment straightforward on any Node.js hosting.
9
+
10
+ ## Build your app
11
+
12
+ ```bash
13
+ npx velojs build
14
+ ```
15
+
16
+ This runs two Vite builds:
17
+ 1. **Client build** → `dist/client/` (JavaScript, CSS, and other static assets)
18
+ 2. **Server build** → `dist/server.js` (the SSR server entry point)
19
+
20
+ ## Start in production
21
+
22
+ ```bash
23
+ npx velojs start
24
+ ```
25
+
26
+ This automatically sets `NODE_ENV=production` and starts the server. In production mode, VeloJS serves static files from `dist/client/` and handles SSR for all page routes.
27
+
28
+ ## Dockerfile
29
+
30
+ Here's a production-ready Dockerfile with multi-stage build:
31
+
32
+ ```dockerfile
33
+ FROM node:22-alpine AS builder
34
+ WORKDIR /app
35
+ COPY package.json package-lock.json ./
36
+ RUN npm ci
37
+ COPY app ./app
38
+ COPY tsconfig.json vite.config.ts ./
39
+ RUN npm run build
40
+
41
+ FROM node:22-alpine
42
+ WORKDIR /app
43
+ COPY package.json package-lock.json ./
44
+ RUN npm ci --omit=dev
45
+ COPY --from=builder /app/dist ./dist
46
+
47
+ ENV PORT=3000
48
+ EXPOSE 3000
49
+ CMD ["npx", "velojs", "start"]
50
+ ```
51
+
52
+ The multi-stage build keeps the final image small — it only includes production dependencies and the compiled output, not your source code or dev dependencies.
53
+
54
+ ## Setting the port
55
+
56
+ The server reads the port from, in order of precedence:
57
+
58
+ 1. The `PORT` environment variable — the convention most hosts (Railway, Render, Fly, Heroku) inject automatically.
59
+ 2. The `port` field in `defineConfig` (`vite.config.ts`) — applies to both `velojs dev` and `velojs start`.
60
+ 3. The default, `3000`.
61
+
62
+ ```ts
63
+ // vite.config.ts
64
+ import { defineConfig } from "vite";
65
+ import { veloPlugin } from "@mauroandre/velojs/vite";
66
+
67
+ export default defineConfig({
68
+ plugins: [veloPlugin({ port: 4000 })],
69
+ });
70
+ ```
71
+
72
+ In dev, `velojs dev --port 5000` still overrides everything (it's passed straight to Vite).
73
+
74
+ ## Static assets on CDN
75
+
76
+ If you want to serve static assets (JS, CSS, images) from a CDN or S3 bucket instead of the Node.js server, set the `STATIC_BASE_URL` environment variable:
77
+
78
+ ```bash
79
+ STATIC_BASE_URL=https://cdn.example.com/assets npx velojs start
80
+ ```
81
+
82
+ The `<Scripts />` component and CSS `url()` references will automatically use this prefix. The server will skip serving static files when `STATIC_BASE_URL` points to an external URL.
83
+
84
+ ## Included dependencies
85
+
86
+ VeloJS is an opinionated framework — a single `npm install @mauroandre/velojs` brings everything you need:
87
+
88
+ - **Hono** — HTTP server and routing
89
+ - **Preact** — UI rendering (SSR + client hydration)
90
+ - **@preact/signals** — Reactive state management
91
+ - **wouter-preact** — Client-side routing
92
+ - **Vite** — Build tool and dev server
93
+ - **@preact/preset-vite** — Preact JSX support
94
+ - **@hono/vite-dev-server** — SSR dev server
95
+
96
+ You don't need to install or configure any of these separately. VeloJS controls the versions to ensure everything works together.
@@ -0,0 +1,209 @@
1
+ ---
2
+ name: velojs-routes
3
+ description: "The route tree in `app/routes.tsx`: nesting, layouts, path-less wrappers, URL params, index routes, the catch-all 404, per-route status codes. Use when adding/moving/removing a page or URL, building a layout, or asked why a route 404s."
4
+ ---
5
+
6
+ # Routes
7
+
8
+ In VeloJS, all routes are defined in a single file: `app/routes.tsx`. Instead of filesystem-based routing, you define your routes as a **tree structure**. This gives you full control over layouts, nesting, and middleware inheritance.
9
+
10
+ ## The route tree
11
+
12
+ Each node in the tree represents either a **page** (leaf node) or a **layout** (node with children). Here's an example with authentication:
13
+
14
+ ```typescript
15
+ import type { AppRoutes } from "@mauroandre/velojs";
16
+ import * as Root from "./client-root.js";
17
+ import * as AuthLayout from "./auth/Layout.js";
18
+ import * as Login from "./auth/Login.js";
19
+ import * as AdminLayout from "./admin/Layout.js";
20
+ import * as Dashboard from "./admin/Dashboard.js";
21
+ import * as Users from "./admin/Users.js";
22
+ import * as UserDetail from "./admin/UserDetail.js";
23
+ import { authMiddleware } from "./modules/auth/auth.middleware.js";
24
+
25
+ export default [
26
+ {
27
+ module: Root,
28
+ isRoot: true,
29
+ children: [
30
+ // Public routes — no authentication required
31
+ {
32
+ module: AuthLayout,
33
+ children: [
34
+ { path: "/login", module: Login },
35
+ ],
36
+ },
37
+ // Protected routes — authMiddleware runs first
38
+ {
39
+ module: AdminLayout,
40
+ middlewares: [authMiddleware],
41
+ children: [
42
+ { path: "/", module: Dashboard },
43
+ { path: "/users", module: Users },
44
+ { path: "/users/:id", module: UserDetail },
45
+ ],
46
+ },
47
+ ],
48
+ },
49
+ ] satisfies AppRoutes;
50
+ ```
51
+
52
+ ## How nesting works
53
+
54
+ Routes with `children` act as **layouts**. Their `Component` receives `children` and wraps the nested routes. VeloJS renders the full hierarchy from root to leaf.
55
+
56
+ For example, when a user visits `/users/123`, VeloJS renders:
57
+
58
+ ```
59
+ Root (isRoot — the <html>, <head>, <body> shell)
60
+ └─ AdminLayout (sidebar, navigation bar)
61
+ └─ UserDetail (the actual page content)
62
+ ```
63
+
64
+ Here's what the layout components look like:
65
+
66
+ ```typescript
67
+ // app/admin/Layout.tsx — Layout wraps its children
68
+ export const Component = ({ children }: { children: any }) => (
69
+ <div class="layout">
70
+ <nav class="sidebar">...</nav>
71
+ <main class="content">{children}</main>
72
+ </div>
73
+ );
74
+
75
+ // app/admin/UserDetail.tsx — Page is the leaf (no children)
76
+ export const Component = () => {
77
+ const { data } = useLoader<User>();
78
+ return <div>{data.value?.name}</div>;
79
+ };
80
+ ```
81
+
82
+ Every layout and page can have its own `loader`. When a page is requested, **all loaders in the hierarchy run in parallel** — the Root loader, AdminLayout loader, and UserDetail loader all execute at the same time. This makes data loading fast.
83
+
84
+ On the client, a layout **stays mounted** while you navigate between its child routes — only the inner content swaps. This follows the tree automatically: navigating between sibling routes never re-mounts anything above them, so a sidebar or nav bar (and its loader, scroll, and state) is preserved instead of flickering. It works the same whether the layout has a `path` (a section like `/admin`) or is a path-less wrapper (an app shell grouping several top-level routes) — no configuration needed.
85
+
86
+ ## Route node properties
87
+
88
+ | Property | Type | Description |
89
+ |----------|------|-------------|
90
+ | `path` | `string` | URL path segment. Supports `:params` (e.g., `/users/:id`). |
91
+ | `module` | `RouteModule` | The module with `Component`, `loader`, and `action_*` exports. |
92
+ | `children` | `RouteNode[]` | Nested routes. When present, the module acts as a layout. |
93
+ | `middlewares` | `MiddlewareHandler[]` | Server-side middlewares. Inherited by all children. |
94
+ | `isRoot` | `boolean` | Marks the root node (the HTML shell). |
95
+ | `statusCode` | `number` | HTTP status code for the rendered page. Defaults to `200`. See [Status codes](#status-codes) and [The 404 page](#the-404-page). |
96
+
97
+ ## Status codes
98
+
99
+ By default every page responds with HTTP `200`. Set `statusCode` on a route to override it — useful for pages like _gone_ (`410`) or _service unavailable_ (`503`):
100
+
101
+ ```typescript
102
+ { path: "/old-page", module: Gone, statusCode: 410 }
103
+ ```
104
+
105
+ The status is applied to both the SSR HTML response and the JSON returned for SPA navigation, so it stays consistent however the page is reached.
106
+
107
+ For a **conditional** status — a route that exists but whose resource may be missing (e.g. `/users/:id` for an unknown id) — don't use a static `statusCode`. Set it from the loader instead, via the Hono context:
108
+
109
+ ```typescript
110
+ export const loader = async ({ params, c }: LoaderArgs) => {
111
+ const user = await getUser(params.id);
112
+ if (!user) c.status(404);
113
+ return { user };
114
+ };
115
+ ```
116
+
117
+ ## The 404 page
118
+
119
+ To render your own _not found_ page, add a **catch-all** route with `path: "*"` as the **last child of the root**:
120
+
121
+ ```typescript
122
+ import * as NotFound from "./pages/NotFound.js";
123
+
124
+ export default [
125
+ {
126
+ module: Root,
127
+ isRoot: true,
128
+ children: [
129
+ { path: "/", module: Home },
130
+ // Keep the catch-all last — it matches only when nothing else does.
131
+ { path: "*", module: NotFound, statusCode: 404 },
132
+ ],
133
+ },
134
+ ] satisfies AppRoutes;
135
+ ```
136
+
137
+ The `NotFound` page is a normal page component, rendered inside the root layout like any other route:
138
+
139
+ ```typescript
140
+ // app/pages/NotFound.tsx
141
+ export const Component = () => (
142
+ <main>
143
+ <h1>404</h1>
144
+ <p>Page not found.</p>
145
+ <a href="/">Go home</a>
146
+ </main>
147
+ );
148
+ ```
149
+
150
+ VeloJS handles the catch-all specially across all modes:
151
+
152
+ - **SSR** — it's wired to Hono's `notFound` hook, so it runs only after no route matched and no static asset was found. It never shadows your assets, actions, streams, or sockets. The response carries the `statusCode` (default `404`).
153
+ - **SPA navigation** — the client router renders it when no route matches, with no round-trip. A top-level unknown URL renders standalone; an unknown sub-route of a section that has a layout (e.g. `/admin/nope`) renders the 404 **inside that section's layout**, so the layout stays mounted (no flicker).
154
+ - **Static generation** (`velojs build --static`) — it's emitted as `dist/404.html` at the output root, following the universal `/404.html` convention that static hosts (nginx, GitHub Pages, Netlify, S3/CloudFront…) serve automatically.
155
+
156
+ The catch-all is **optional**. Without it, an unmatched path still responds `404`, just with a plain-text body instead of your styled page.
157
+
158
+ > On the server the 404 always renders standalone (inside the root shell only) and its route middlewares don't run — there was no route to match. On the client, a section layout stays mounted when you navigate to a bad sub-route of that section.
159
+
160
+ ## Path resolution
161
+
162
+ Paths are **segments** that build up from parent to child:
163
+
164
+ ```
165
+ Root (no path)
166
+ └─ AdminLayout (no path)
167
+ ├─ Dashboard → path: "/" → fullPath: "/"
168
+ ├─ Users → path: "/users" → fullPath: "/users"
169
+ └─ UserDetail → path: "/users/:id" → fullPath: "/users/:id"
170
+ ```
171
+
172
+ Nodes without a `path` don't add a segment to the URL — they are **pure layout wrappers**. This is useful when you want to wrap a group of pages in a shared layout without changing their URLs.
173
+
174
+ The Vite plugin automatically parses your `routes.tsx` at build time and calculates both `fullPath` (the complete URL) and `path` (the relative segment) for each module.
175
+
176
+ ## Shared layouts, different paths
177
+
178
+ You can reuse the same layout for different route groups:
179
+
180
+ ```typescript
181
+ export default [
182
+ {
183
+ module: Root,
184
+ isRoot: true,
185
+ children: [
186
+ // Public pages — PublicLayout, no auth
187
+ {
188
+ module: PublicLayout,
189
+ children: [
190
+ { path: "/", module: Home },
191
+ { path: "/about", module: About },
192
+ ],
193
+ },
194
+ // Dashboard — DashboardLayout + auth middleware
195
+ {
196
+ path: "/dashboard",
197
+ module: DashboardLayout,
198
+ middlewares: [authMiddleware],
199
+ children: [
200
+ { path: "/", module: Overview },
201
+ { path: "/settings", module: Settings },
202
+ ],
203
+ },
204
+ ],
205
+ },
206
+ ] satisfies AppRoutes;
207
+ ```
208
+
209
+ Here, `PublicLayout` and `DashboardLayout` are completely separate layouts. Public pages live at `/` and `/about`, while dashboard pages live at `/dashboard` and `/dashboard/settings`.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: velojs-scripts-component
3
+ description: "The `<Scripts />` component in the root HTML shell: injecting the client bundle and CSS, favicon, CDN/static asset base paths. Use when setting up the root layout, changing the favicon, or serving assets from a CDN."
4
+ ---
5
+
6
+ # Scripts Component
7
+
8
+ The `Scripts` component injects the CSS and JavaScript files that your app needs. It must be placed inside `<head>` in your `client-root.tsx`.
9
+
10
+ ## Usage
11
+
12
+ ```tsx
13
+ import { Scripts } from "@mauroandre/velojs";
14
+
15
+ export const Component = ({ children }) => (
16
+ <html>
17
+ <head>
18
+ <Scripts />
19
+ </head>
20
+ <body>{children}</body>
21
+ </html>
22
+ );
23
+ ```
24
+
25
+ ## What it outputs
26
+
27
+ `Scripts` automatically detects whether you're in development or production mode and outputs the appropriate tags.
28
+
29
+ **In development:**
30
+ ```html
31
+ <link rel="icon" href="/favicon.ico" type="image/x-icon" />
32
+ <script type="module" src="/@vite/client"></script>
33
+ <script type="module" src="/__velo_client.js"></script>
34
+ ```
35
+
36
+ **In production:**
37
+ ```html
38
+ <link rel="icon" href="/favicon.ico" type="image/x-icon" />
39
+ <link rel="stylesheet" href="/client.C3f9Ax_1.css" />
40
+ <script type="module" src="/client.C3f9Ax_1.js"></script>
41
+ ```
42
+
43
+ ## Props
44
+
45
+ | Prop | Type | Default | Description |
46
+ |------|------|---------|-------------|
47
+ | `basePath` | `string` | `""` | Base path prefix for all asset URLs. Uses `STATIC_BASE_URL` env var if set. |
48
+ | `favicon` | `string \| false` | `"/favicon.ico"` | Path to the favicon. Set to `false` to disable. |
49
+
50
+ ## Using with a CDN
51
+
52
+ If you serve static assets from a CDN or S3 bucket, set the `STATIC_BASE_URL` environment variable. The `Scripts` component will automatically prefix all asset URLs:
53
+
54
+ ```bash
55
+ STATIC_BASE_URL=https://cdn.example.com/assets velojs start
56
+ ```
57
+
58
+ This makes the output become:
59
+ ```html
60
+ <link rel="stylesheet" href="https://cdn.example.com/assets/client.C3f9Ax_1.css" />
61
+ <script type="module" src="https://cdn.example.com/assets/client.C3f9Ax_1.js"></script>
62
+ ```
@@ -0,0 +1,155 @@
1
+ ---
2
+ name: velojs-server-api
3
+ description: "Server lifecycle and escape hatches: `addRoutes` for custom Hono routes, `onServer` for the raw HTTP server, port config, env vars. Use when mounting a custom Hono route, attaching a library to the HTTP server, or configuring the port."
4
+ ---
5
+
6
+ # Server API
7
+
8
+ VeloJS gives you direct access to the underlying Hono server. This lets you add custom API endpoints, SSE streams, WebSocket handlers, and anything else that goes beyond page routes.
9
+
10
+ ## addRoutes
11
+
12
+ Register custom Hono routes in `app/server.tsx`. These routes are added **before** page and action routes, so they take priority:
13
+
14
+ ```typescript
15
+ import { addRoutes } from "@mauroandre/velojs/server";
16
+ import type { Hono } from "hono";
17
+
18
+ addRoutes((app: Hono) => {
19
+ // Simple API endpoint
20
+ app.get("/api/health", (c) => c.json({ ok: true }));
21
+
22
+ // File upload
23
+ app.post("/api/upload", async (c) => {
24
+ const body = await c.req.parseBody();
25
+ const file = body.file;
26
+ return c.json({ ok: true });
27
+ });
28
+
29
+ // Middleware for a group of API routes
30
+ app.use("/api/admin/*", async (c, next) => {
31
+ const token = c.req.header("Authorization");
32
+ if (!token) return c.json({ error: "Unauthorized" }, 401);
33
+ await next();
34
+ });
35
+ });
36
+ ```
37
+
38
+ ## Server-Sent Events (SSE)
39
+
40
+ > **Recommended:** For most real-time use cases, use [Event Streams](/docs/event-streams) (`createEventStream`). They handle channels, snapshots, auto-close, heartbeat, middleware inheritance, and type safety automatically. Drop down to raw `streamSSE` only when you need fine-grained control over the protocol.
41
+
42
+ SSE is a simple way to push real-time updates from server to client. If you need raw control, use Hono's `streamSSE` directly:
43
+
44
+ ```typescript
45
+ import { addRoutes } from "@mauroandre/velojs/server";
46
+
47
+ addRoutes((app) => {
48
+ app.get("/api/events", async (c) => {
49
+ const { streamSSE } = await import("hono/streaming");
50
+
51
+ return streamSSE(c, async (stream) => {
52
+ // Send initial data when client connects
53
+ await stream.writeSSE({
54
+ event: "snapshot",
55
+ data: JSON.stringify({ count: 0 }),
56
+ });
57
+
58
+ // Subscribe to updates
59
+ const unsubscribe = subscribe((data) => {
60
+ stream.writeSSE({
61
+ event: "update",
62
+ data: JSON.stringify(data),
63
+ });
64
+ });
65
+
66
+ // Clean up when client disconnects
67
+ stream.onAbort(() => { unsubscribe(); });
68
+
69
+ // Keep the stream open
70
+ await new Promise<void>(() => {});
71
+ });
72
+ });
73
+ });
74
+ ```
75
+
76
+ On the client, use the standard `EventSource` API:
77
+
78
+ ```typescript
79
+ useEffect(() => {
80
+ const es = new EventSource("/api/events");
81
+
82
+ es.addEventListener("snapshot", (e) => {
83
+ state.value = JSON.parse(e.data);
84
+ });
85
+
86
+ es.addEventListener("update", (e) => {
87
+ state.value = JSON.parse(e.data);
88
+ });
89
+
90
+ return () => es.close();
91
+ }, []);
92
+ ```
93
+
94
+ ### SSE with polling
95
+
96
+ For live metrics or periodic updates, use a polling loop inside the stream:
97
+
98
+ ```typescript
99
+ addRoutes((app) => {
100
+ app.get("/api/metrics/live", async (c) => {
101
+ const { streamSSE } = await import("hono/streaming");
102
+
103
+ return streamSSE(c, async (stream) => {
104
+ let running = true;
105
+ stream.onAbort(() => { running = false; });
106
+
107
+ while (running) {
108
+ const metrics = await collectMetrics();
109
+ await stream.writeSSE({ data: JSON.stringify(metrics) });
110
+ await new Promise((r) => setTimeout(r, 3000));
111
+ }
112
+ });
113
+ });
114
+ });
115
+ ```
116
+
117
+ ## onServer — WebSocket support
118
+
119
+ `onServer` gives you access to the underlying Node.js HTTP server. This is mainly used for WebSocket upgrade handlers:
120
+
121
+ ```typescript
122
+ import { onServer } from "@mauroandre/velojs/server";
123
+
124
+ onServer(async (httpServer) => {
125
+ const { WebSocketServer } = await import("ws");
126
+ const wss = new WebSocketServer({ noServer: true });
127
+
128
+ httpServer.on("upgrade", (req, socket, head) => {
129
+ const url = new URL(req.url!, `http://${req.headers.host}`);
130
+
131
+ if (url.pathname === "/ws") {
132
+ wss.handleUpgrade(req, socket, head, (ws) => {
133
+ ws.on("message", (raw) => {
134
+ const msg = JSON.parse(raw.toString());
135
+ // Handle message
136
+ });
137
+
138
+ ws.on("close", () => {
139
+ // Clean up
140
+ });
141
+ });
142
+ }
143
+ });
144
+ });
145
+ ```
146
+
147
+ Callbacks registered with `onServer` queue until the server starts. If called after the server is already running, the callback executes immediately.
148
+
149
+ ## Environment variables
150
+
151
+ | Variable | Default | Description |
152
+ |----------|---------|-------------|
153
+ | `PORT` | `3000` | The port the server listens on. Overrides `defineConfig`'s `port`. |
154
+ | `NODE_ENV` | — | Set automatically by `velojs start` to `production` |
155
+ | `STATIC_BASE_URL` | `""` | CDN/bucket prefix for static assets |