@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.
- package/README.md +20 -10
- 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-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/client.js +58 -47
- package/dist/components.js +18 -16
- package/dist/hooks.d.ts +3 -2
- package/dist/hooks.js +107 -110
- package/dist/loader-store.d.ts +49 -0
- package/dist/loader-store.js +103 -0
- package/dist/server.js +11 -11
- package/dist/testing/createTestApp.d.ts +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/types.d.ts +7 -3
- package/dist/vite.js +276 -277
- package/package.json +6 -2
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: velojs-getting-started
|
|
3
|
+
description: "Scaffold a new VeloJS project and its file layout: `velojs init`, `vite.config.ts`, `app/routes.tsx`, the root HTML shell, dev server. Use when starting a project from scratch or wiring up the toolchain."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Getting Started
|
|
7
|
+
|
|
8
|
+
VeloJS is a fullstack web framework that combines Hono (server), Preact (UI), and Vite (build) into a single, cohesive experience. You write your pages, loaders, and server actions in one place — VeloJS handles the rest.
|
|
9
|
+
|
|
10
|
+
## Create a new project
|
|
11
|
+
|
|
12
|
+
The fastest way to start is with the CLI:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @mauroandre/velojs init my-app
|
|
16
|
+
cd my-app
|
|
17
|
+
npm install
|
|
18
|
+
npx velojs dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This creates a ready-to-run project. Open `http://localhost:3000` and you should see your app.
|
|
22
|
+
|
|
23
|
+
## Project structure
|
|
24
|
+
|
|
25
|
+
Every VeloJS project follows this layout:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
my-app/
|
|
29
|
+
├── app/
|
|
30
|
+
│ ├── routes.tsx # Where you define all your routes
|
|
31
|
+
│ ├── server.tsx # Server-only init (databases, APIs, etc)
|
|
32
|
+
│ ├── client.tsx # Client-only init (global CSS, etc)
|
|
33
|
+
│ ├── client-root.tsx # The HTML shell (<html>, <head>, <body>)
|
|
34
|
+
│ └── pages/ # Your pages, layouts, and modules
|
|
35
|
+
├── vite.config.ts # Vite configuration (just one plugin)
|
|
36
|
+
├── tsconfig.json
|
|
37
|
+
└── package.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Let's walk through each file.
|
|
41
|
+
|
|
42
|
+
## vite.config.ts
|
|
43
|
+
|
|
44
|
+
VeloJS works through a single Vite plugin that handles everything — SSR, route scanning, code transforms, and the dev server:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { defineConfig } from "vite";
|
|
48
|
+
import { veloPlugin } from "@mauroandre/velojs/vite";
|
|
49
|
+
|
|
50
|
+
export default defineConfig({
|
|
51
|
+
plugins: [veloPlugin()],
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
That's it. No extra plugins needed.
|
|
56
|
+
|
|
57
|
+
## package.json scripts
|
|
58
|
+
|
|
59
|
+
Your project needs three scripts:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"scripts": {
|
|
64
|
+
"dev": "velojs dev",
|
|
65
|
+
"build": "velojs build",
|
|
66
|
+
"start": "velojs start"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- `dev` — starts the development server with hot reload
|
|
72
|
+
- `build` — compiles the client bundle and server entry for production
|
|
73
|
+
- `start` — runs the production server (automatically sets `NODE_ENV=production`)
|
|
74
|
+
|
|
75
|
+
## app/client-root.tsx — The HTML shell
|
|
76
|
+
|
|
77
|
+
This is the outermost component of your app. It renders the `<html>`, `<head>`, and `<body>` tags. Every page in your app will be rendered inside this shell.
|
|
78
|
+
|
|
79
|
+
The `<Scripts />` component is required — it injects the CSS and JavaScript that your app needs (Vite HMR in dev, compiled assets in production).
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import type { ComponentChildren } from "preact";
|
|
83
|
+
import { Scripts } from "@mauroandre/velojs";
|
|
84
|
+
|
|
85
|
+
export const Component = ({ children }: { children?: ComponentChildren }) => (
|
|
86
|
+
<html lang="en">
|
|
87
|
+
<head>
|
|
88
|
+
<meta charset="UTF-8" />
|
|
89
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
90
|
+
<title>My App</title>
|
|
91
|
+
<Scripts />
|
|
92
|
+
</head>
|
|
93
|
+
<body>{children}</body>
|
|
94
|
+
</html>
|
|
95
|
+
);
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## app/client.tsx — Client entry
|
|
99
|
+
|
|
100
|
+
This file runs only on the client (browser). Use it to import global CSS or initialize client-side libraries:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import "./styles/global.css";
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## app/server.tsx — Server entry
|
|
107
|
+
|
|
108
|
+
This file runs only on the server. Use it to connect to databases, register custom API routes, or start background jobs:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { addRoutes } from "@mauroandre/velojs/server";
|
|
112
|
+
import type { Hono } from "hono";
|
|
113
|
+
|
|
114
|
+
// Register custom API routes
|
|
115
|
+
addRoutes((app: Hono) => {
|
|
116
|
+
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## app/routes.tsx — Route definitions
|
|
121
|
+
|
|
122
|
+
Routes are defined as a tree. Each node has a `module` (the page or layout component) and optionally `path`, `children`, and `middlewares`:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import type { AppRoutes } from "@mauroandre/velojs";
|
|
126
|
+
import * as Root from "./client-root.js";
|
|
127
|
+
import * as Home from "./pages/Home.js";
|
|
128
|
+
|
|
129
|
+
export default [
|
|
130
|
+
{
|
|
131
|
+
module: Root,
|
|
132
|
+
isRoot: true,
|
|
133
|
+
children: [
|
|
134
|
+
{ path: "/", module: Home },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
] satisfies AppRoutes;
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Page and layout modules **must** be imported with `import * as Name from "./x.js"`. This isn't style: when the plugin resolves each module's URL it only reads namespace imports, so a default or named import silently drops the route — the server logs `Module … has no fullPath` at startup and the page 404s. Use `./`-relative paths inside `appDirectory` too; a `../` path or a tsconfig alias produces a key the plugin can't match, with the same result.
|
|
141
|
+
|
|
142
|
+
## Your first page
|
|
143
|
+
|
|
144
|
+
A page in VeloJS can export three things:
|
|
145
|
+
|
|
146
|
+
- `Component` — the Preact component that renders the UI
|
|
147
|
+
- `loader` — a server-side function that fetches data
|
|
148
|
+
- `action_*` — server-side functions callable from the client
|
|
149
|
+
|
|
150
|
+
Here's a simple page with a loader:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import type { LoaderArgs } from "@mauroandre/velojs";
|
|
154
|
+
import { useLoader } from "@mauroandre/velojs/hooks";
|
|
155
|
+
|
|
156
|
+
// This runs on the server. It fetches data for the page.
|
|
157
|
+
export const loader = async ({ c }: LoaderArgs) => {
|
|
158
|
+
return { message: "Hello, VeloJS!" };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// This runs on both server (SSR) and client (hydration).
|
|
162
|
+
export const Component = () => {
|
|
163
|
+
const { data } = useLoader<{ message: string }>();
|
|
164
|
+
return <h1>{data.value?.message}</h1>;
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The loader runs on the server, and the data is automatically available in the component through `useLoader()`. On the first page load (SSR), the data is injected into the HTML. On client-side navigation, it's fetched via an API call — all automatically.
|
|
169
|
+
|
|
170
|
+
## Run the dev server
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm run dev
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Open `http://localhost:3000` in your browser. Changes to your code will hot-reload instantly.
|
|
177
|
+
|
|
178
|
+
## Configuration
|
|
179
|
+
|
|
180
|
+
The `veloPlugin()` accepts optional configuration:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
veloPlugin({
|
|
184
|
+
appDirectory: "./app", // where your app files live (default: "./app")
|
|
185
|
+
routesFile: "routes.tsx", // your routes file (default: "routes.tsx")
|
|
186
|
+
serverInit: "server.tsx", // server init file (default: "server.tsx")
|
|
187
|
+
clientInit: "client.tsx", // client init file (default: "client.tsx")
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Most projects don't need to change these defaults.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: velojs-hooks
|
|
3
|
+
description: "Client/SSR hooks: `useLoader`, `useParams`, `useQuery`, `usePathname`, `useNavigate`, `touch`, `useEventStream`, `useSocket`. Use when reading URL params or query string, navigating programmatically, or updating signal state."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hooks
|
|
7
|
+
|
|
8
|
+
VeloJS provides a set of hooks that work identically on both server (SSR) and client. On the server, they read from `AsyncLocalStorage` (isolated per request). On the client, they use the DOM and wouter-preact.
|
|
9
|
+
|
|
10
|
+
## Available hooks
|
|
11
|
+
|
|
12
|
+
| Hook | Returns | Description |
|
|
13
|
+
|------|---------|-------------|
|
|
14
|
+
| `useLoader<T>(deps?)` | `{ data, loading, refetch }` | Page data with SSR + SPA support |
|
|
15
|
+
| `Loader<T>()` | `{ data, loading }` | Module-level SSR-only data |
|
|
16
|
+
| `useEventStream<T, S>(stream, opts?)` | `{ data, snapshot, closed, error }` | Real-time SSE stream subscription |
|
|
17
|
+
| `useParams<T>()` | `T` | URL parameters (`:id`, etc) |
|
|
18
|
+
| `useQuery<T>()` | `T` | Query string parameters |
|
|
19
|
+
| `useNavigate()` | `navigate(path)` | Programmatic navigation |
|
|
20
|
+
| `usePathname()` | `string` | Absolute URL pathname |
|
|
21
|
+
| `touch(signal)` | `void` | Force signal notification after mutation |
|
|
22
|
+
|
|
23
|
+
## useParams
|
|
24
|
+
|
|
25
|
+
Reads URL parameters from the current route. For example, if your route path is `/users/:id`:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
const params = useParams<{ id: string }>();
|
|
29
|
+
console.log(params.id); // "123" when visiting /users/123
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## useQuery
|
|
33
|
+
|
|
34
|
+
Reads query string parameters from the URL. For example, visiting `/search?q=hello&page=2`:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
const query = useQuery<{ q: string; page: string }>();
|
|
38
|
+
console.log(query.q); // "hello"
|
|
39
|
+
console.log(query.page); // "2"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## useNavigate
|
|
43
|
+
|
|
44
|
+
Returns a function for programmatic navigation (when you need to navigate from code, not from a link):
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const navigate = useNavigate();
|
|
48
|
+
|
|
49
|
+
const handleLogin = async () => {
|
|
50
|
+
const result = await action_login({ body: credentials });
|
|
51
|
+
if (!result.error) {
|
|
52
|
+
navigate("/admin");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## usePathname
|
|
58
|
+
|
|
59
|
+
Returns the **absolute** pathname of the current URL, and works identically on the server (SSR) and the client. It reads the real URL rather than any router state, so you always get the full path.
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
const pathname = usePathname();
|
|
63
|
+
// Always returns the full path, e.g., "/admin/users/123"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This is useful when you need the exact current URL — for active-link styling, breadcrumbs, analytics, and the like.
|
|
67
|
+
|
|
68
|
+
## useEventStream
|
|
69
|
+
|
|
70
|
+
Subscribes to a server-sent event stream created with `createEventStream`. Returns reactive signals for the latest event, initial snapshot, close state, and errors.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const { data, snapshot, closed, error } = useEventStream(stream, {
|
|
74
|
+
channel: "deploy-123", // optional
|
|
75
|
+
enabled: true, // optional, default true
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// data.value — latest event received from the server
|
|
79
|
+
// snapshot.value — initial state on connect (if configured)
|
|
80
|
+
// closed.value — true when server closed the stream (closeOn matched)
|
|
81
|
+
// error.value — parse or connection error, if any
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The hook handles `EventSource` lifecycle automatically: opens on mount, closes on unmount, re-opens when `channel` changes. See [Event Streams](/docs/event-streams) for the full guide.
|
|
85
|
+
|
|
86
|
+
## touch
|
|
87
|
+
|
|
88
|
+
Preact signals are great for reactivity, but they have a limitation: mutating a nested property (like changing `items[0].checked`) doesn't trigger a signal update. The `touch` function forces the signal to notify its subscribers after a mutation:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { useSignal } from "@preact/signals";
|
|
92
|
+
import { touch } from "@mauroandre/velojs/hooks";
|
|
93
|
+
|
|
94
|
+
const items = useSignal<Item[]>([]);
|
|
95
|
+
|
|
96
|
+
// Mutating a nested property — signal doesn't know about this
|
|
97
|
+
items.value[0].checked = true;
|
|
98
|
+
|
|
99
|
+
// touch() creates a shallow copy, which triggers the update
|
|
100
|
+
touch(items);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Use `touch` whenever you mutate nested properties on a signal value instead of replacing the entire value.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: velojs-link-component
|
|
3
|
+
description: "The `<Link>` component for SPA navigation, linking by route module, and substituting `:params`. Use when adding navigation, a menu, or any internal link."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Link Component
|
|
7
|
+
|
|
8
|
+
The `Link` component handles navigation in VeloJS. It supports both simple string paths and type-safe module references.
|
|
9
|
+
|
|
10
|
+
All link paths are **root-absolute** — they resolve from the root of your app, exactly as the server does. There is no per-layout relative context to keep track of.
|
|
11
|
+
|
|
12
|
+
## Basic usage
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { Link } from "@mauroandre/velojs";
|
|
16
|
+
|
|
17
|
+
// Simple string path
|
|
18
|
+
<Link to="/users">Users</Link>
|
|
19
|
+
|
|
20
|
+
// With URL parameters
|
|
21
|
+
<Link to="/users/123">View User</Link>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Module references
|
|
25
|
+
|
|
26
|
+
Instead of hardcoding paths, you can pass a route module directly. The link uses the module's `fullPath`, so if the path changes in `routes.tsx`, your links update automatically:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { Link } from "@mauroandre/velojs";
|
|
30
|
+
import * as UserPage from "./users/UserDetail.js";
|
|
31
|
+
|
|
32
|
+
// Navigates to the module's full path (e.g. /users/:id)
|
|
33
|
+
<Link to={UserPage} params={{ id: "123" }}>View</Link>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Parameters and query strings
|
|
37
|
+
|
|
38
|
+
Use `params` to substitute `:param` placeholders in the path, and `search` to add query string parameters:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
<Link to={UserPage} params={{ id: "123" }} search={{ tab: "settings" }}>
|
|
42
|
+
User Settings
|
|
43
|
+
</Link>
|
|
44
|
+
// Renders: /users/123?tab=settings
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## The `~/` prefix
|
|
48
|
+
|
|
49
|
+
String paths may start with `~/`. This is wouter-preact's root-absolute prefix: the `~` is stripped and the path resolves from the root. Since VeloJS links are already root-absolute, `~/x` and `/x` resolve to the same place:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
<Link to="~/stacks">Stacks</Link> // → /stacks
|
|
53
|
+
<Link to="/stacks">Stacks</Link> // → /stacks (identical)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The prefix is supported for backward compatibility — you can keep existing `~/` links as-is, or drop the `~` in new code.
|
|
57
|
+
|
|
58
|
+
> **Migrating from older versions:** earlier releases resolved string and module links relative to the current layout's nest context, and `~/` escaped to the root. Layout nesting on the client now mirrors the server (full-path matching, no nested base), so every link is root-absolute. In practice: keep using absolute paths (`/stacks`) or module references; existing `~/` links keep working.
|
|
59
|
+
|
|
60
|
+
## Props reference
|
|
61
|
+
|
|
62
|
+
| Prop | Type | Description |
|
|
63
|
+
|------|------|-------------|
|
|
64
|
+
| `to` | `string \| RouteModule` | Destination path or module. A string may start with `~/` (root-absolute); a module resolves to its `fullPath`. |
|
|
65
|
+
| `params` | `Record<string, string>` | Substitutes `:param` placeholders in the path. |
|
|
66
|
+
| `search` | `Record<string, string>` | Appends query string parameters to the URL. |
|
|
67
|
+
| `absolute` | `boolean` | Deprecated, no-op. Module references always use `fullPath`. Kept for backward compatibility. |
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: velojs-loaders
|
|
3
|
+
description: "Loading server data for a page: the `loader` export, `useLoader`, the `Loader` handle for sharing a layout's data with its children, hydration and SPA refetch. Use when a page needs data from a database or API before it renders."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Loaders
|
|
7
|
+
|
|
8
|
+
Loaders are how you fetch data on the server and make it available to your components. VeloJS provides two patterns, each designed for a different use case.
|
|
9
|
+
|
|
10
|
+
## useLoader — Component-level data
|
|
11
|
+
|
|
12
|
+
Use `useLoader` inside a `Component` when you need data that's specific to that page. It supports both SSR (server-side rendering) and SPA navigation (client-side fetch).
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
export const Component = () => {
|
|
16
|
+
const { data, loading, refetch } = useLoader<MyType>();
|
|
17
|
+
|
|
18
|
+
// data: Signal<T | null> — the loader data (reactive)
|
|
19
|
+
// loading: Signal<boolean> — true while fetching
|
|
20
|
+
// refetch: () => void — manually re-fetch the data
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**How it works:**
|
|
25
|
+
- On the first page load (SSR), the data is rendered on the server and injected into the HTML
|
|
26
|
+
- When the user navigates to this page via a link (SPA), `useLoader` fetches the data from the server automatically
|
|
27
|
+
- The data is a Preact signal — your UI updates automatically when it changes
|
|
28
|
+
|
|
29
|
+
### Re-fetching on route changes — automatic
|
|
30
|
+
|
|
31
|
+
If your page's route declares a param, you don't have to do anything. VeloJS reads the route tree, so it already knows that a module at `/users/:id` depends on `id`, and refreshes its data when you navigate from `/users/1` to `/users/2`:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// app/users/Detail.tsx — routed at /users/:id
|
|
35
|
+
export const Component = () => {
|
|
36
|
+
const { data } = useLoader<User>(); // refreshes on :id change, no deps needed
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Deps — for what the route doesn't declare
|
|
41
|
+
|
|
42
|
+
`routes.tsx` declares path params (`/users/:id`), never query strings or client state. When your data depends on one of those, pass it as a dependency:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const query = useQuery<{ tab: string }>();
|
|
46
|
+
const { data } = useLoader<Report>([query.tab]); // ?tab= is not in the route
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
A dep that just repeats a path param is harmless but redundant — the framework already knows.
|
|
50
|
+
|
|
51
|
+
## Loader — Module-level shared data
|
|
52
|
+
|
|
53
|
+
Use `Loader` (capital L, outside of components) when **another module needs your data**. It gives you a handle you can export — typically from a layout, so its child pages can read it.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// app/admin/Layout.tsx
|
|
57
|
+
import { Loader } from "@mauroandre/velojs/hooks";
|
|
58
|
+
|
|
59
|
+
export const loader = async ({ c }: LoaderArgs) => {
|
|
60
|
+
const user = c.get("user");
|
|
61
|
+
return { name: user.name, role: user.role };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Runs at module import — not inside a component
|
|
65
|
+
export const { data: userData, refetch } = Loader<UserData>();
|
|
66
|
+
|
|
67
|
+
export const Component = ({ children }) => (
|
|
68
|
+
<div>
|
|
69
|
+
<header>Hello, {userData.value?.name}</header>
|
|
70
|
+
{children}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Child pages import the handle directly:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// app/admin/Dashboard.tsx
|
|
79
|
+
import { userData } from "./Layout.js";
|
|
80
|
+
|
|
81
|
+
export const Component = () => (
|
|
82
|
+
<div>Role: {userData.value?.role}</div>
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`Loader` and `useLoader` for the same module return the **same signal**, so a layout's data and a child's read of it are one value. A child can update it after a mutation without a round-trip:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { userData } from "./Layout.js";
|
|
90
|
+
import { touch } from "@mauroandre/velojs/hooks";
|
|
91
|
+
|
|
92
|
+
const handleSave = async () => {
|
|
93
|
+
const saved = await action_save({ body: { name: name.value } });
|
|
94
|
+
userData.value!.name = saved.name; // you already know the result
|
|
95
|
+
touch(userData); // notify — see the hooks doc
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Or let the server recompute, when the new state isn't something the client can derive:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import { refetch } from "./Layout.js";
|
|
103
|
+
await action_reset({ body: { id } });
|
|
104
|
+
refetch(); // e.g. aggregate counters
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> **Never mirror loader data into a module-level `export let`.** On the server, a module binding is per-process, not per-request — a value stored there belongs to whichever request wrote it last. `Loader`'s server value is an AsyncLocalStorage getter, so it is per-request by construction. That's the whole reason the handle is safe to export.
|
|
108
|
+
|
|
109
|
+
## When to use which
|
|
110
|
+
|
|
111
|
+
| Scenario | Use |
|
|
112
|
+
|----------|-----|
|
|
113
|
+
| Page-specific data (list of users, post details) | `useLoader` |
|
|
114
|
+
| Data another module reads (a layout's list, the current record) | `Loader` |
|
|
115
|
+
| Data that changes when the route's params change | either — automatic |
|
|
116
|
+
| Data that depends on a query string or client state | `useLoader` with deps |
|
|
117
|
+
| Refresh after a mutation | `refetch()` (server recomputes) or `touch()` (you already know) |
|
|
118
|
+
|
|
119
|
+
## How data flows
|
|
120
|
+
|
|
121
|
+
Understanding the data flow helps you debug issues:
|
|
122
|
+
|
|
123
|
+
**First page load (SSR):**
|
|
124
|
+
1. All loaders in the route hierarchy run in parallel on the server
|
|
125
|
+
2. The server injects every module's data into the HTML as `window.__PAGE_DATA__`, keyed by module
|
|
126
|
+
3. `useLoader()` and `Loader()` read it — non-destructively, so any number of components can read the same module
|
|
127
|
+
|
|
128
|
+
**Client-side navigation (SPA):**
|
|
129
|
+
1. User clicks a link
|
|
130
|
+
2. VeloJS compares each module's route pattern against the new URL and marks stale the ones whose **declared params** changed
|
|
131
|
+
3. One `?_data=1` request serves all of them
|
|
132
|
+
4. Only the stale modules are written — a module the client owns locally (a filtered list, an optimistic edit) is left alone
|
|
133
|
+
|
|
134
|
+
That last point is why a layout at `/admin/empresas` keeps a filtered list while you navigate among its children: its route declares no param, so nothing invalidates it.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: velojs-middlewares
|
|
3
|
+
description: "Route middleware: auth guards, redirects, per-route server logic, passing values via `c.set`/`c.get`. Use when protecting a route, requiring login, checking permissions, or running server logic before a page or action."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Middlewares
|
|
7
|
+
|
|
8
|
+
Middlewares are server-side functions that run before your loaders and actions. They're perfect for authentication, authorization, logging, and other cross-cutting concerns. The Vite plugin automatically removes them from the client bundle.
|
|
9
|
+
|
|
10
|
+
## Creating a middleware
|
|
11
|
+
|
|
12
|
+
Use `createMiddleware` from `@mauroandre/velojs/factory`. It wraps Hono's middleware API:
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// app/modules/auth/auth.middleware.ts
|
|
16
|
+
import { createMiddleware } from "@mauroandre/velojs/factory";
|
|
17
|
+
import { getCookie } from "@mauroandre/velojs/cookie";
|
|
18
|
+
|
|
19
|
+
export const authMiddleware = createMiddleware(async (c, next) => {
|
|
20
|
+
const token = getCookie(c, "session");
|
|
21
|
+
|
|
22
|
+
if (!token) {
|
|
23
|
+
// No token: redirect GET requests, return 401 for API calls
|
|
24
|
+
if (c.req.method === "GET") return c.redirect("/login");
|
|
25
|
+
return c.json({ error: "unauthorized" }, 401);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Verify the token and attach user data to the context
|
|
29
|
+
const user = await verifyToken(token);
|
|
30
|
+
c.set("user", user);
|
|
31
|
+
|
|
32
|
+
// Continue to the next middleware or handler
|
|
33
|
+
await next();
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Using in routes
|
|
38
|
+
|
|
39
|
+
Add the `middlewares` property to any route node. All children of that node will inherit the middleware:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export default [
|
|
43
|
+
{
|
|
44
|
+
module: Root,
|
|
45
|
+
isRoot: true,
|
|
46
|
+
children: [
|
|
47
|
+
// Public routes — no middleware
|
|
48
|
+
{
|
|
49
|
+
module: AuthLayout,
|
|
50
|
+
children: [
|
|
51
|
+
{ path: "/login", module: Login },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
// Protected routes — authMiddleware applies to everything below
|
|
55
|
+
{
|
|
56
|
+
module: AdminLayout,
|
|
57
|
+
middlewares: [authMiddleware],
|
|
58
|
+
children: [
|
|
59
|
+
{ path: "/", module: Dashboard },
|
|
60
|
+
{ path: "/stacks", module: Stacks },
|
|
61
|
+
|
|
62
|
+
// Admin-only routes — both middlewares apply
|
|
63
|
+
{
|
|
64
|
+
path: "/master",
|
|
65
|
+
module: MasterLayout,
|
|
66
|
+
middlewares: [masterMiddleware],
|
|
67
|
+
children: [
|
|
68
|
+
{ path: "/workers", module: Workers },
|
|
69
|
+
{ path: "/settings", module: Settings },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
] satisfies AppRoutes;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Inheritance
|
|
80
|
+
|
|
81
|
+
Middlewares **accumulate** from parent to child. In the example above:
|
|
82
|
+
|
|
83
|
+
- `/` and `/stacks` → only `authMiddleware` runs
|
|
84
|
+
- `/master/workers` and `/master/settings` → `authMiddleware` runs first, then `masterMiddleware`
|
|
85
|
+
|
|
86
|
+
### What middlewares protect
|
|
87
|
+
|
|
88
|
+
A middleware on a layout guards **every request** that reaches a nested route, including:
|
|
89
|
+
|
|
90
|
+
| Request type | URL pattern | When it happens |
|
|
91
|
+
|--------------|-------------|-----------------|
|
|
92
|
+
| Page load | `GET /stacks` | User opens the page directly (first visit, refresh) |
|
|
93
|
+
| Data fetch | `GET /stacks?_data=1` | SPA navigation — `useLoader` fetches JSON |
|
|
94
|
+
| Action call | `POST /_action/admin/Stacks/delete` | User triggers a server action |
|
|
95
|
+
|
|
96
|
+
You don't need to add the middleware to each route manually — declaring it once on the layout covers all three paths automatically. This is especially important for authentication: without it, your API endpoints could be reached even if the page itself is protected.
|
|
97
|
+
|
|
98
|
+
### Example: protecting an action via layout middleware
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// app/routes.tsx
|
|
102
|
+
{
|
|
103
|
+
module: AdminLayout,
|
|
104
|
+
middlewares: [authMiddleware],
|
|
105
|
+
children: [
|
|
106
|
+
{ path: "/stacks", module: Stacks },
|
|
107
|
+
],
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// app/admin/Stacks.tsx
|
|
113
|
+
export const action_delete = async ({ body, c }: ActionArgs<{ id: string }>) => {
|
|
114
|
+
// authMiddleware already ran — user is guaranteed to be authenticated
|
|
115
|
+
const user = c!.get("user");
|
|
116
|
+
// ... delete logic
|
|
117
|
+
};
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
When the client calls `action_delete({ body: { id: "abc" } })`, it hits `POST /_action/admin/Stacks/delete`. Hono runs `authMiddleware` before the action executes. If the middleware returns an error (e.g., 401), the action never runs.
|
|
121
|
+
|
|
122
|
+
## Sharing data between middleware and handlers
|
|
123
|
+
|
|
124
|
+
Use Hono's `c.set()` in the middleware and `c.get()` in your loaders and actions:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Middleware sets the user
|
|
128
|
+
c.set("user", { id: "123", name: "Mauro", role: "master" });
|
|
129
|
+
|
|
130
|
+
// Loader reads it
|
|
131
|
+
export const loader = async ({ c }: LoaderArgs) => {
|
|
132
|
+
const user = c.get("user");
|
|
133
|
+
return { greeting: `Hello, ${user.name}` };
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Action reads it
|
|
137
|
+
export const action_save = async ({ body, c }: ActionArgs<{ name: string }>) => {
|
|
138
|
+
const user = c!.get("user");
|
|
139
|
+
// user is available here
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This is the recommended pattern for passing authentication data (current user, permissions, etc) from middleware to your page code.
|