@mauroandre/velojs 0.0.37 → 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.
- package/README.md +19 -9
- package/agent/AGENTS.md +143 -0
- package/agent/skills/velojs-actions/SKILL.md +192 -0
- package/agent/skills/velojs-components/SKILL.md +99 -0
- package/agent/skills/velojs-endpoints/SKILL.md +187 -0
- package/agent/skills/velojs-event-streams/SKILL.md +705 -0
- package/agent/skills/velojs-getting-started/SKILL.md +191 -0
- package/agent/skills/velojs-hooks/SKILL.md +103 -0
- package/agent/skills/velojs-link-component/SKILL.md +67 -0
- package/agent/skills/velojs-loaders/SKILL.md +134 -0
- package/agent/skills/velojs-middlewares/SKILL.md +143 -0
- package/agent/skills/velojs-production-deploy/SKILL.md +96 -0
- package/agent/skills/velojs-project-graph/SKILL.md +97 -0
- package/agent/skills/velojs-routes/SKILL.md +209 -0
- package/agent/skills/velojs-scripts-component/SKILL.md +62 -0
- package/agent/skills/velojs-server-api/SKILL.md +155 -0
- package/agent/skills/velojs-sockets/SKILL.md +303 -0
- package/agent/skills/velojs-static-generation/SKILL.md +148 -0
- package/agent/skills/velojs-testing/SKILL.md +472 -0
- package/agent/skills/velojs-type-reference/SKILL.md +142 -0
- package/agent/skills/velojs-vite-plugin/SKILL.md +71 -0
- package/dist/cli.js +42 -31
- package/dist/graph.d.ts +37 -0
- package/dist/graph.js +202 -0
- package/dist/init.js +16 -7
- package/dist/vite.js +279 -248
- 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,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
|
|
@@ -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 |
|