@cfdez11/vex 0.1.0 → 0.2.0
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 +409 -1101
- package/bin/vex.js +41 -0
- package/package.json +5 -1
- package/server/utils/component-processor.js +9 -19
- package/server/utils/files.js +74 -4
- package/server/utils/router.js +2 -0
package/README.md
CHANGED
|
@@ -1,1383 +1,691 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cfdez11/vex
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@cfdez11/vex)
|
|
4
4
|
[](#)
|
|
5
5
|
[](#)
|
|
6
6
|
|
|
7
|
-
A
|
|
8
|
-
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
- [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- [Available Functions](#available-functions)
|
|
32
|
-
- [`reactive(value)`](#reactivevalue)
|
|
33
|
-
- [`effect(fn)`](#effectfn)
|
|
34
|
-
- [`computed(getter)`](#computedgetter)
|
|
35
|
-
- [`watch(source, callback, options)`](#watchsource-callback-options)
|
|
36
|
-
- [In Components](#in-components)
|
|
37
|
-
- [Reactivity Comparison](#reactivity-comparison)
|
|
38
|
-
- [📝 Template Syntax](#-template-syntax)
|
|
39
|
-
- [Interpolation](#interpolation)
|
|
40
|
-
- [Conditionals](#conditionals)
|
|
41
|
-
- [Lists](#lists)
|
|
42
|
-
- [Event Handlers](#event-handlers)
|
|
43
|
-
- [Attributes](#attributes)
|
|
44
|
-
- [🛣️ Routing](#️-routing)
|
|
45
|
-
- [Auto-Generated Routes](#auto-generated-routes)
|
|
46
|
-
- [Dynamic Routes](#dynamic-routes)
|
|
47
|
-
- [Client-Side Navigation](#client-side-navigation)
|
|
48
|
-
- [Accessing Route Parameters](#accessing-route-parameters)
|
|
49
|
-
- [⚡ Prefetching](#-prefetching)
|
|
50
|
-
- [Automatic Prefetching](#automatic-prefetching)
|
|
51
|
-
- [🎨 Styling](#-styling)
|
|
52
|
-
- [🔧 Framework API](#-framework-api)
|
|
53
|
-
- [Component Props](#component-props)
|
|
54
|
-
- [Reactive State](#reactive-state)
|
|
55
|
-
- [Navigation Utilities](#navigation-utilities)
|
|
56
|
-
- [📦 Available Scripts](#-available-scripts)
|
|
57
|
-
- [🏗️ Rendering Flow](#️-rendering-flow)
|
|
58
|
-
- [SSR (Server-Side Rendering)](#ssr-server-side-rendering)
|
|
59
|
-
- [CSR (Client-Side Rendering)](#csr-client-side-rendering)
|
|
60
|
-
- [ISR (Incremental Static Regeneration)](#isr-incremental-static-regeneration)
|
|
61
|
-
- [Server Startup](#server-startup)
|
|
62
|
-
- [Build Process](#build-process)
|
|
63
|
-
- [Client Hydration](#client-hydration)
|
|
64
|
-
- [🗺️ Roadmap](#️-roadmap)
|
|
65
|
-
|
|
66
|
-
## ✨ Key Features
|
|
67
|
-
|
|
68
|
-
- 🚀 **Multiple Rendering Strategies**: SSR, CSR, SSG, and ISR support
|
|
69
|
-
- ⚡ **Auto-Generated Routes**: File-based routing with dynamic routes `[param]`
|
|
70
|
-
- 🔄 **Reactive System**: Vue-like reactivity with `reactive()` and `computed()`
|
|
71
|
-
- 🧩 **Component-Based**: Reusable `.html` components (server & client)
|
|
72
|
-
- 🎭 **Streaming & Suspense**: Progressive loading with fallback UI
|
|
73
|
-
- 📐 **Nested Layouts**: Custom layouts per route
|
|
74
|
-
- 🔗 **Smart Prefetching**: Automatic page prefetching on link hover
|
|
75
|
-
- 💾 **Built-in Caching**: Server and client-side caching
|
|
76
|
-
- 🎨 **Tailwind CSS**: Integrated styling solution
|
|
77
|
-
- 📝 **Template Syntax**: Familiar directives (`v-if`, `v-for`, `@click`, etc.)
|
|
78
|
-
- 🌐 **SPA Navigation**: Client-side routing without page reloads
|
|
79
|
-
- 🔌 **Zero Config**: No manual route registration needed
|
|
80
|
-
- 📦 **Pure JavaScript**: No TypeScript to focus on core functionality without build complexity
|
|
81
|
-
|
|
82
|
-
## �📁 Project Structure
|
|
7
|
+
A vanilla JavaScript meta-framework built on Express.js with file-based routing, multiple rendering strategies (SSR, CSR, SSG, ISR), streaming Suspense, and a Vue-like reactive system — no TypeScript, no bundler.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Quick Start](#-quick-start)
|
|
13
|
+
- [Project Structure](#-project-structure)
|
|
14
|
+
- [Configuration](#-configuration-vexconfigjson)
|
|
15
|
+
- [Creating a Page](#-creating-a-page)
|
|
16
|
+
- [Components](#-components)
|
|
17
|
+
- [Rendering Strategies](#-rendering-strategies)
|
|
18
|
+
- [Layouts](#-layouts)
|
|
19
|
+
- [Suspense (Streaming)](#-suspense-streaming)
|
|
20
|
+
- [Reactive System](#-reactive-system)
|
|
21
|
+
- [Template Syntax](#-template-syntax)
|
|
22
|
+
- [Routing](#️-routing)
|
|
23
|
+
- [Prefetching](#-prefetching)
|
|
24
|
+
- [Styling](#-styling)
|
|
25
|
+
- [Framework API](#-framework-api)
|
|
26
|
+
- [Available Scripts](#-available-scripts)
|
|
27
|
+
- [Rendering Flow](#️-rendering-flow)
|
|
28
|
+
- [Roadmap](#️-roadmap)
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
83
31
|
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
│ ├── layout.html # Main layout (header, footer, etc.)
|
|
87
|
-
│ ├── page.html # Home page
|
|
88
|
-
│ ├── error/page.html # Error page
|
|
89
|
-
│ ├── not-found/page.html # 404 page
|
|
90
|
-
│ ├── page-csr/ # CSR example
|
|
91
|
-
│ │ ├── page.html # CSR main page
|
|
92
|
-
│ │ └── [city]/page.html # Dynamic CSR route
|
|
93
|
-
│ ├── page-ssr/ # SSR example
|
|
94
|
-
│ │ ├── page.html # SSR main page
|
|
95
|
-
│ │ └── [city]/page.html # Dynamic SSR route
|
|
96
|
-
│ ├── static/ # Static page example
|
|
97
|
-
│ │ ├── layout.html # Static layout
|
|
98
|
-
│ │ └── page.html # Static page
|
|
99
|
-
│ └── static-with-data/ # Static with data example
|
|
100
|
-
│ └── page.html # Static page with data fetching
|
|
101
|
-
├── components/ # Component definitions (.html files)
|
|
102
|
-
│ ├── counter.html # Counter component
|
|
103
|
-
│ ├── user-card.html # User card component
|
|
104
|
-
│ ├── user-card-delayed.html # Delayed user card (for suspense demo)
|
|
105
|
-
│ ├── user-card-skeleton.html # Skeleton placeholder
|
|
106
|
-
│ ├── weather.html # Weather component
|
|
107
|
-
│ └── weather/ # Weather sub-components
|
|
108
|
-
│ ├── weather-links.html
|
|
109
|
-
│ ├── weather-params.html
|
|
110
|
-
│ └── weather-state.html
|
|
111
|
-
└── .app/ # Framework files (do not edit)
|
|
112
|
-
├── client/ # Client-side framework
|
|
113
|
-
│ ├── services/ # Client framework core
|
|
114
|
-
│ │ ├── reactive.js # Reactivity system
|
|
115
|
-
│ │ ├── html.js # Template literal helpers
|
|
116
|
-
│ │ ├── hydrate.js # Component hydration
|
|
117
|
-
│ │ ├── cache.js # Client-side caching
|
|
118
|
-
│ │ ├── navigation/ # Navigation utilities
|
|
119
|
-
│ │ │ ├── router.js # Client router
|
|
120
|
-
│ │ │ ├── navigate.js # Navigation API
|
|
121
|
-
│ │ │ ├── prefetch.js # Page prefetching
|
|
122
|
-
│ │ │ ├── metadata.js # Dynamic metadata
|
|
123
|
-
│ │ │ └── ...
|
|
124
|
-
│ │ └── _routes.js # Auto-generated routes
|
|
125
|
-
│ ├── _components/ # Auto-generated component scripts
|
|
126
|
-
│ ├── styles.css # Compiled Tailwind styles
|
|
127
|
-
│ └── favicon.ico # Favicon
|
|
128
|
-
└── server/ # Server-side framework
|
|
129
|
-
├── index.js # Entry point
|
|
130
|
-
├── root.html # Root HTML template
|
|
131
|
-
├── _cache/ # Server-side cache
|
|
132
|
-
└── utils/ # Server utilities
|
|
133
|
-
├── router.js # Router and SSR rendering
|
|
134
|
-
├── component-processor.js # Component processing
|
|
135
|
-
├── template.js # Template rendering
|
|
136
|
-
├── streaming.js # Suspense and streaming
|
|
137
|
-
├── cache.js # Server-side caching
|
|
138
|
-
├── files.js # File system utilities
|
|
139
|
-
└── _routes.js # Auto-generated routes
|
|
32
|
+
```bash
|
|
33
|
+
npm install @cfdez11/vex
|
|
140
34
|
```
|
|
141
35
|
|
|
36
|
+
Requires **Node.js >= 18**.
|
|
37
|
+
|
|
142
38
|
## 🚀 Quick Start
|
|
143
39
|
|
|
144
40
|
```bash
|
|
145
|
-
|
|
146
|
-
|
|
41
|
+
mkdir my-app && cd my-app
|
|
42
|
+
npm init -y
|
|
43
|
+
npm install @cfdez11/vex
|
|
44
|
+
npm install -D tailwindcss npm-run-all
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Update `package.json`:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"type": "module",
|
|
52
|
+
"scripts": {
|
|
53
|
+
"dev": "run-p dev:*",
|
|
54
|
+
"dev:app": "vex dev",
|
|
55
|
+
"dev:css": "npx @tailwindcss/cli -i ./src/input.css -o ./public/styles.css --watch",
|
|
56
|
+
"build": "vex build",
|
|
57
|
+
"start": "vex start"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Create the minimum structure:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mkdir -p pages src public
|
|
66
|
+
echo '@import "tailwindcss";' > src/input.css
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm run dev
|
|
71
|
+
# → http://localhost:3001
|
|
72
|
+
```
|
|
147
73
|
|
|
148
|
-
|
|
149
|
-
pnpm dev
|
|
74
|
+
## 📁 Project Structure
|
|
150
75
|
|
|
151
|
-
|
|
152
|
-
|
|
76
|
+
```
|
|
77
|
+
my-app/
|
|
78
|
+
├── pages/ # File-based routes
|
|
79
|
+
│ ├── layout.vex # Root layout (wraps all pages)
|
|
80
|
+
│ ├── page.vex # Home page → /
|
|
81
|
+
│ ├── about/page.vex # About page → /about
|
|
82
|
+
│ ├── users/[id]/page.vex # Dynamic → /users/:id
|
|
83
|
+
│ ├── not-found/page.vex # 404 handler
|
|
84
|
+
│ └── error/page.vex # 500 handler
|
|
85
|
+
├── components/ # Reusable .vex components (any subfolder works)
|
|
86
|
+
├── utils/ # User utilities (e.g. delay.js)
|
|
87
|
+
├── public/ # Static assets served at /
|
|
88
|
+
│ └── styles.css # Compiled Tailwind output
|
|
89
|
+
├── src/
|
|
90
|
+
│ └── input.css # Tailwind entry point
|
|
91
|
+
├── root.html # HTML shell template (optional override)
|
|
92
|
+
└── vex.config.json # Framework config (optional)
|
|
153
93
|
```
|
|
154
94
|
|
|
155
|
-
|
|
95
|
+
> Generated files are written to `.vexjs/` — do not edit them manually.
|
|
156
96
|
|
|
157
|
-
|
|
97
|
+
### Custom source directory
|
|
98
|
+
|
|
99
|
+
If you prefer to keep all app code in a subfolder, set `srcDir` in `vex.config.json`:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
my-app/
|
|
103
|
+
├── app/ ← srcDir: "app"
|
|
104
|
+
│ ├── pages/
|
|
105
|
+
│ └── components/
|
|
106
|
+
├── public/
|
|
107
|
+
└── vex.config.json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## ⚙️ Configuration (`vex.config.json`)
|
|
111
|
+
|
|
112
|
+
Optional file at the project root.
|
|
158
113
|
|
|
159
|
-
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"srcDir": "app",
|
|
117
|
+
"watchIgnore": ["dist", "coverage"]
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| Field | Type | Default | Description |
|
|
122
|
+
|-------|------|---------|-------------|
|
|
123
|
+
| `srcDir` | `string` | `"."` | Directory containing `pages/`, `components/` and all user `.vex` files. When set, the dev watcher only observes this folder instead of the whole project root. |
|
|
124
|
+
| `watchIgnore` | `string[]` | `[]` | Additional directory names to exclude from the dev file watcher. Merged with the built-in list: `node_modules`, `dist`, `build`, `.git`, `.vexjs`, `coverage`, `.next`, `.nuxt`, `tmp`, and more. |
|
|
125
|
+
|
|
126
|
+
## 📄 Creating a Page
|
|
160
127
|
|
|
161
128
|
```html
|
|
162
|
-
<!-- pages/example/page.
|
|
129
|
+
<!-- pages/example/page.vex -->
|
|
163
130
|
<script server>
|
|
164
|
-
|
|
165
|
-
import UserCard from "components/user-card.html";
|
|
131
|
+
import UserCard from "components/user-card.vex";
|
|
166
132
|
|
|
167
|
-
|
|
168
|
-
|
|
133
|
+
const metadata = { title: "My Page", description: "Page description" };
|
|
134
|
+
|
|
135
|
+
async function getData({ req }) {
|
|
169
136
|
return { message: "Hello from the server" };
|
|
170
137
|
}
|
|
171
|
-
|
|
172
|
-
// Page metadata
|
|
173
|
-
const metadata = {
|
|
174
|
-
title: "My Page",
|
|
175
|
-
description: "Page description",
|
|
176
|
-
};
|
|
177
138
|
</script>
|
|
178
139
|
|
|
179
140
|
<script client>
|
|
180
|
-
|
|
181
|
-
import Counter from "components/counter.html";
|
|
141
|
+
import Counter from "components/counter.vex";
|
|
182
142
|
</script>
|
|
183
143
|
|
|
184
144
|
<template>
|
|
185
145
|
<h1>{{message}}</h1>
|
|
186
146
|
<Counter start="0" />
|
|
187
|
-
<UserCard userId="
|
|
147
|
+
<UserCard :userId="1" />
|
|
188
148
|
</template>
|
|
189
149
|
```
|
|
190
150
|
|
|
191
|
-
|
|
151
|
+
Routes are auto-generated from the `pages/` folder — no manual registration needed.
|
|
192
152
|
|
|
193
153
|
## 🧩 Components
|
|
194
154
|
|
|
195
|
-
Components are
|
|
155
|
+
Components are `.vex` files. They can live in any folder; the default convention is `components/`.
|
|
196
156
|
|
|
197
|
-
### Component
|
|
157
|
+
### Component structure
|
|
198
158
|
|
|
199
159
|
```html
|
|
200
|
-
<!-- components/counter.
|
|
160
|
+
<!-- components/counter.vex -->
|
|
201
161
|
<script client>
|
|
202
|
-
import { reactive, computed } from "
|
|
203
|
-
|
|
204
|
-
// Component props
|
|
205
|
-
const props = vprops({
|
|
206
|
-
start: { default: 10 },
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// Reactive state
|
|
210
|
-
const counter = reactive(props.start);
|
|
211
|
-
|
|
212
|
-
// Methods
|
|
213
|
-
function increment() {
|
|
214
|
-
counter.value++;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function decrement() {
|
|
218
|
-
counter.value--;
|
|
219
|
-
}
|
|
162
|
+
import { reactive, computed } from "vex/reactive";
|
|
220
163
|
|
|
221
|
-
|
|
222
|
-
const
|
|
164
|
+
const props = xprops({ start: { default: 0 } });
|
|
165
|
+
const count = reactive(props.start);
|
|
166
|
+
const stars = computed(() => "⭐".repeat(count.value));
|
|
223
167
|
</script>
|
|
224
168
|
|
|
225
169
|
<template>
|
|
226
|
-
<div
|
|
227
|
-
<button @click="
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
<
|
|
231
|
-
<button @click="increment">
|
|
232
|
-
Add
|
|
233
|
-
</button>
|
|
234
|
-
<div>{{stars.join('')}}</div>
|
|
170
|
+
<div>
|
|
171
|
+
<button @click="count.value--">-</button>
|
|
172
|
+
<span>{{count.value}}</span>
|
|
173
|
+
<button @click="count.value++">+</button>
|
|
174
|
+
<div>{{stars.value}}</div>
|
|
235
175
|
</div>
|
|
236
176
|
</template>
|
|
237
177
|
```
|
|
238
178
|
|
|
239
|
-
### Server
|
|
240
|
-
|
|
241
|
-
Server components are rendered on the backend and support async data fetching:
|
|
179
|
+
### Server components
|
|
242
180
|
|
|
243
181
|
```html
|
|
244
|
-
<!-- components/user-card.
|
|
182
|
+
<!-- components/user-card.vex -->
|
|
245
183
|
<script server>
|
|
246
|
-
const props =
|
|
247
|
-
userId: { required: true },
|
|
248
|
-
});
|
|
184
|
+
const props = xprops({ userId: { default: null } });
|
|
249
185
|
|
|
250
|
-
async function getData() {
|
|
186
|
+
async function getData({ props }) {
|
|
251
187
|
const user = await fetch(`https://api.example.com/users/${props.userId}`)
|
|
252
|
-
.then(
|
|
188
|
+
.then(r => r.json());
|
|
253
189
|
return { user };
|
|
254
190
|
}
|
|
255
191
|
</script>
|
|
256
192
|
|
|
257
193
|
<template>
|
|
258
|
-
<div
|
|
194
|
+
<div>
|
|
259
195
|
<h3>{{user.name}}</h3>
|
|
260
196
|
<p>{{user.email}}</p>
|
|
261
197
|
</div>
|
|
262
198
|
</template>
|
|
263
199
|
```
|
|
264
200
|
|
|
265
|
-
### Using
|
|
201
|
+
### Using components
|
|
266
202
|
|
|
267
|
-
Import
|
|
203
|
+
Import them in any page or component:
|
|
268
204
|
|
|
269
205
|
```html
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
import Counter from "components/counter.html";
|
|
206
|
+
<script server>
|
|
207
|
+
import UserCard from "components/user-card.vex";
|
|
273
208
|
</script>
|
|
274
209
|
|
|
275
|
-
<script
|
|
276
|
-
import
|
|
210
|
+
<script client>
|
|
211
|
+
import Counter from "components/counter.vex";
|
|
277
212
|
</script>
|
|
278
213
|
|
|
279
214
|
<template>
|
|
280
|
-
<Counter start="5" />
|
|
281
|
-
<UserCard userId="
|
|
215
|
+
<Counter :start="5" />
|
|
216
|
+
<UserCard :userId="1" />
|
|
282
217
|
</template>
|
|
283
218
|
```
|
|
284
219
|
|
|
285
|
-
|
|
220
|
+
### Component props (`xprops`)
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
const props = xprops({
|
|
224
|
+
userId: { default: null },
|
|
225
|
+
label: { default: "Click me" },
|
|
226
|
+
});
|
|
227
|
+
```
|
|
286
228
|
|
|
287
|
-
|
|
229
|
+
Pass them from the parent template:
|
|
288
230
|
|
|
289
|
-
|
|
231
|
+
```html
|
|
232
|
+
<UserCard :userId="user.id" label="Profile" />
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## 🎭 Rendering Strategies
|
|
290
236
|
|
|
291
|
-
|
|
237
|
+
Configured via `metadata` in `<script server>`.
|
|
292
238
|
|
|
293
|
-
|
|
239
|
+
### SSR — Server-Side Rendering (default)
|
|
240
|
+
|
|
241
|
+
Rendered fresh on every request. Best for dynamic, personalised or SEO-critical pages.
|
|
294
242
|
|
|
295
243
|
```html
|
|
296
|
-
<!-- pages/page-ssr/page.html -->
|
|
297
244
|
<script server>
|
|
245
|
+
const metadata = { title: "Live Data" };
|
|
246
|
+
|
|
298
247
|
async function getData() {
|
|
299
|
-
|
|
300
|
-
const data = await fetch('https://api.example.com/data').then(r => r.json());
|
|
248
|
+
const data = await fetch("https://api.example.com/data").then(r => r.json());
|
|
301
249
|
return { data };
|
|
302
250
|
}
|
|
303
|
-
|
|
304
|
-
const metadata = {
|
|
305
|
-
title: "SSR Page",
|
|
306
|
-
description: "Server-rendered on every request"
|
|
307
|
-
};
|
|
308
251
|
</script>
|
|
309
252
|
|
|
310
253
|
<template>
|
|
311
254
|
<h1>{{data.title}}</h1>
|
|
312
|
-
<p>Generated at: {{new Date().toISOString()}}</p>
|
|
313
255
|
</template>
|
|
314
256
|
```
|
|
315
257
|
|
|
316
|
-
|
|
317
|
-
- ✅ Fresh data on every request
|
|
318
|
-
- ✅ Best for SEO (fully rendered HTML)
|
|
319
|
-
- ✅ Fast initial page load
|
|
320
|
-
- ⚠️ Server load on every request
|
|
321
|
-
|
|
322
|
-
### CSR - Client-Side Rendering
|
|
258
|
+
### CSR — Client-Side Rendering
|
|
323
259
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
Minimal HTML is sent from the server. All rendering happens in the browser using JavaScript.
|
|
260
|
+
No server-rendered HTML. The page fetches its own data in the browser. Use for highly interactive or authenticated areas.
|
|
327
261
|
|
|
328
262
|
```html
|
|
329
|
-
<!-- pages/page-csr/page.html -->
|
|
330
263
|
<script client>
|
|
331
|
-
import { reactive } from "
|
|
332
|
-
|
|
264
|
+
import { reactive } from "vex/reactive";
|
|
265
|
+
|
|
333
266
|
const data = reactive(null);
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
async function loadData() {
|
|
337
|
-
const response = await fetch('https://api.example.com/data');
|
|
338
|
-
data.value = await response.json();
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
loadData();
|
|
267
|
+
|
|
268
|
+
fetch("/api/data").then(r => r.json()).then(v => data.value = v);
|
|
342
269
|
</script>
|
|
343
270
|
|
|
344
271
|
<template>
|
|
345
|
-
<div
|
|
346
|
-
<h1>{{data.title}}</h1>
|
|
347
|
-
</div>
|
|
348
|
-
<div v-else>
|
|
349
|
-
Loading...
|
|
272
|
+
<div x-if="data.value">
|
|
273
|
+
<h1>{{data.value.title}}</h1>
|
|
350
274
|
</div>
|
|
275
|
+
<div x-if="!data.value">Loading…</div>
|
|
351
276
|
</template>
|
|
352
277
|
```
|
|
353
278
|
|
|
354
|
-
|
|
355
|
-
- ✅ Highly interactive
|
|
356
|
-
- ✅ Reduced server load
|
|
357
|
-
- ✅ Instant navigation after first load
|
|
358
|
-
- ⚠️ Slower initial render
|
|
359
|
-
- ⚠️ Less SEO-friendly
|
|
360
|
-
|
|
361
|
-
### SSG - Static Site Generation
|
|
279
|
+
### SSG — Static Site Generation
|
|
362
280
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
Pages are pre-rendered at build time and served as static HTML. No server processing on requests.
|
|
281
|
+
Rendered once and cached forever. Best for content that rarely changes.
|
|
366
282
|
|
|
367
283
|
```html
|
|
368
|
-
<!-- pages/static-with-data/page.html -->
|
|
369
284
|
<script server>
|
|
285
|
+
const metadata = { title: "Docs", static: true };
|
|
286
|
+
|
|
370
287
|
async function getData() {
|
|
371
|
-
|
|
372
|
-
const data = await fetch('https://api.example.com/content').then(r => r.json());
|
|
373
|
-
return { data };
|
|
288
|
+
return { content: await fetchDocs() };
|
|
374
289
|
}
|
|
375
|
-
|
|
376
|
-
const metadata = {
|
|
377
|
-
title: "Static Page",
|
|
378
|
-
description: "Pre-rendered at build time",
|
|
379
|
-
revalidate: 'never' // Never regenerate
|
|
380
|
-
};
|
|
381
290
|
</script>
|
|
382
|
-
|
|
383
|
-
<template>
|
|
384
|
-
<h1>{{data.title}}</h1>
|
|
385
|
-
<p>Built at: {{new Date().toISOString()}}</p>
|
|
386
|
-
</template>
|
|
387
291
|
```
|
|
388
292
|
|
|
389
|
-
|
|
390
|
-
- ✅ Fastest possible delivery (static files)
|
|
391
|
-
- ✅ Lowest server cost
|
|
392
|
-
- ✅ Perfect for SEO
|
|
393
|
-
- ✅ Can be served from CDN
|
|
394
|
-
- ⚠️ Content only updates on rebuild
|
|
395
|
-
|
|
396
|
-
### ISR - Incremental Static Regeneration
|
|
397
|
-
|
|
398
|
-
**When to use**: Content that changes occasionally (product pages, articles with comments).
|
|
293
|
+
### ISR — Incremental Static Regeneration
|
|
399
294
|
|
|
400
|
-
|
|
295
|
+
Cached but automatically regenerated after N seconds. Best of speed and freshness.
|
|
401
296
|
|
|
402
297
|
```html
|
|
403
|
-
<!-- pages/page-ssr/[city]/page.html -->
|
|
404
298
|
<script server>
|
|
405
|
-
import { useRouteParams } from ".app/navigation/use-route-params.js";
|
|
406
|
-
|
|
407
|
-
async function getData() {
|
|
408
|
-
const { city } = useRouteParams();
|
|
409
|
-
const weather = await fetch(`https://api.weather.com/${city}`).then(r => r.json());
|
|
410
|
-
return { city, weather };
|
|
411
|
-
}
|
|
412
|
-
|
|
413
299
|
const metadata = {
|
|
414
300
|
title: "Weather",
|
|
415
|
-
|
|
416
|
-
revalidate: 10 // Regenerate every 10 seconds
|
|
301
|
+
revalidate: 60, // regenerate every 60 s
|
|
417
302
|
};
|
|
418
|
-
</script>
|
|
419
303
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
304
|
+
async function getData({ req }) {
|
|
305
|
+
const { city } = req.params;
|
|
306
|
+
const weather = await fetchWeather(city);
|
|
307
|
+
return { city, weather };
|
|
308
|
+
}
|
|
309
|
+
</script>
|
|
425
310
|
```
|
|
426
311
|
|
|
427
|
-
|
|
428
|
-
- ✅ Static performance with fresh content
|
|
429
|
-
- ✅ Automatic background regeneration
|
|
430
|
-
- ✅ Best of both worlds (speed + freshness)
|
|
431
|
-
- ✅ Reduces API calls
|
|
432
|
-
- ⚠️ Slightly stale data possible (within revalidation window)
|
|
312
|
+
**`revalidate` values:**
|
|
433
313
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
```
|
|
314
|
+
| Value | Behaviour |
|
|
315
|
+
|-------|-----------|
|
|
316
|
+
| `10` (number) | Regenerate after N seconds |
|
|
317
|
+
| `true` | Regenerate after 60 s |
|
|
318
|
+
| `0` | Stale-while-revalidate (serve cache, regenerate in background) |
|
|
319
|
+
| `false` / `"never"` | Pure SSG — never regenerate |
|
|
320
|
+
| _(omitted)_ | SSR — no caching |
|
|
442
321
|
|
|
443
322
|
## 📐 Layouts
|
|
444
323
|
|
|
445
|
-
|
|
324
|
+
### Root layout
|
|
446
325
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
The main layout is defined in `pages/layout.html` and wraps all pages:
|
|
326
|
+
`pages/layout.vex` wraps every page:
|
|
450
327
|
|
|
451
328
|
```html
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const props = vprops({
|
|
455
|
-
children: { default: null },
|
|
456
|
-
});
|
|
329
|
+
<script server>
|
|
330
|
+
const props = xprops({ children: { default: "" } });
|
|
457
331
|
</script>
|
|
458
332
|
|
|
459
333
|
<template>
|
|
460
|
-
<
|
|
461
|
-
<
|
|
462
|
-
<
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<main>
|
|
470
|
-
{{children}} <!-- Page content injected here -->
|
|
471
|
-
</main>
|
|
472
|
-
|
|
473
|
-
<footer class="bg-gray-800 text-white">
|
|
474
|
-
<p>© 2026 My App</p>
|
|
475
|
-
</footer>
|
|
476
|
-
</div>
|
|
334
|
+
<header>
|
|
335
|
+
<nav>
|
|
336
|
+
<a href="/" data-prefetch>Home</a>
|
|
337
|
+
<a href="/about" data-prefetch>About</a>
|
|
338
|
+
</nav>
|
|
339
|
+
</header>
|
|
340
|
+
<main>{{props.children}}</main>
|
|
341
|
+
<footer>© 2026</footer>
|
|
477
342
|
</template>
|
|
478
343
|
```
|
|
479
344
|
|
|
480
|
-
###
|
|
481
|
-
|
|
482
|
-
You can create custom layouts for specific routes:
|
|
483
|
-
|
|
484
|
-
```html
|
|
485
|
-
<!-- pages/static/layout.html -->
|
|
486
|
-
<script client>
|
|
487
|
-
const props = vprops({
|
|
488
|
-
children: { default: null },
|
|
489
|
-
});
|
|
490
|
-
</script>
|
|
345
|
+
### Nested layouts
|
|
491
346
|
|
|
492
|
-
|
|
493
|
-
<div class="static-layout">
|
|
494
|
-
<aside class="sidebar">
|
|
495
|
-
<!-- Sidebar navigation -->
|
|
496
|
-
</aside>
|
|
497
|
-
<div class="content">
|
|
498
|
-
{{children}} <!-- Page content -->
|
|
499
|
-
</div>
|
|
500
|
-
</div>
|
|
501
|
-
</template>
|
|
502
|
-
```
|
|
347
|
+
Add a `layout.vex` inside any subdirectory:
|
|
503
348
|
|
|
504
|
-
**Layout Hierarchy:**
|
|
505
349
|
```
|
|
506
|
-
pages/
|
|
507
|
-
|
|
508
|
-
|
|
350
|
+
pages/
|
|
351
|
+
layout.vex ← wraps everything
|
|
352
|
+
docs/
|
|
353
|
+
layout.vex ← wraps /docs/* only
|
|
354
|
+
page.vex
|
|
355
|
+
getting-started/page.vex
|
|
509
356
|
```
|
|
510
357
|
|
|
511
358
|
## ⏳ Suspense (Streaming)
|
|
512
359
|
|
|
513
|
-
|
|
360
|
+
Streams a fallback immediately while a slow component loads:
|
|
514
361
|
|
|
515
362
|
```html
|
|
516
363
|
<script server>
|
|
517
|
-
import
|
|
518
|
-
import
|
|
364
|
+
import SlowCard from "components/slow-card.vex";
|
|
365
|
+
import SkeletonCard from "components/skeleton-card.vex";
|
|
519
366
|
</script>
|
|
520
367
|
|
|
521
368
|
<template>
|
|
522
|
-
<Suspense :fallback="<
|
|
523
|
-
<
|
|
369
|
+
<Suspense :fallback="<SkeletonCard />">
|
|
370
|
+
<SlowCard :userId="1" />
|
|
524
371
|
</Suspense>
|
|
525
372
|
</template>
|
|
526
373
|
```
|
|
527
374
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
- The rest of the page is shown immediately
|
|
531
|
-
- Slow components load via streaming
|
|
532
|
-
- Improves perceived performance
|
|
533
|
-
- Better user experience with progressive loading
|
|
375
|
+
The server sends the skeleton on the first flush, then replaces it with the real content via a streamed `<template>` tag when it resolves.
|
|
534
376
|
|
|
535
377
|
## 🔄 Reactive System
|
|
536
378
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
### Available Functions
|
|
540
|
-
|
|
541
|
-
#### `reactive(value)`
|
|
542
|
-
|
|
543
|
-
Creates a reactive proxy that automatically tracks dependencies and triggers effects when changed.
|
|
379
|
+
Mirrors Vue 3's Composition API. Import from `vex/reactive` in `<script client>` blocks.
|
|
544
380
|
|
|
545
|
-
|
|
381
|
+
### `reactive(value)`
|
|
546
382
|
|
|
547
383
|
```js
|
|
548
|
-
import { reactive } from "
|
|
549
|
-
|
|
550
|
-
// Reactive primitives (wrapped in .value)
|
|
551
|
-
const counter = reactive(0);
|
|
552
|
-
const name = reactive("Alice");
|
|
553
|
-
counter.value++; // Triggers UI update
|
|
554
|
-
|
|
555
|
-
// Reactive objects (direct property access)
|
|
556
|
-
const state = reactive({ count: 0, user: "Alice" });
|
|
557
|
-
state.count++; // Triggers UI update
|
|
558
|
-
state.user = "Bob"; // Triggers UI update
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
#### `effect(fn)`
|
|
562
|
-
|
|
563
|
-
Creates a side effect that automatically re-runs when its reactive dependencies change.
|
|
564
|
-
|
|
565
|
-
**Use when:** You need to perform side effects (logging, API calls, DOM manipulation) based on reactive state.
|
|
566
|
-
|
|
567
|
-
```js
|
|
568
|
-
import { reactive, effect } from ".app/reactive.js";
|
|
384
|
+
import { reactive } from "vex/reactive";
|
|
569
385
|
|
|
386
|
+
// Primitives → access via .value
|
|
570
387
|
const count = reactive(0);
|
|
388
|
+
count.value++;
|
|
571
389
|
|
|
572
|
-
//
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
count.value++; // Effect runs again
|
|
579
|
-
count.value++; // Effect runs again
|
|
580
|
-
|
|
581
|
-
cleanup(); // Stop the effect
|
|
390
|
+
// Objects → direct property access
|
|
391
|
+
const state = reactive({ x: 1, name: "Alice" });
|
|
392
|
+
state.x++;
|
|
393
|
+
state.name = "Bob";
|
|
582
394
|
```
|
|
583
395
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
Creates a computed reactive value that automatically recalculates when its dependencies change.
|
|
587
|
-
|
|
588
|
-
**Use when:** You need derived state that depends on other reactive values.
|
|
396
|
+
### `computed(getter)`
|
|
589
397
|
|
|
590
398
|
```js
|
|
591
|
-
import { reactive, computed } from "
|
|
399
|
+
import { reactive, computed } from "vex/reactive";
|
|
592
400
|
|
|
593
401
|
const price = reactive(100);
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
// Computed value automatically updates
|
|
597
|
-
const total = computed(() => price.value * quantity.value);
|
|
402
|
+
const qty = reactive(2);
|
|
403
|
+
const total = computed(() => price.value * qty.value);
|
|
598
404
|
|
|
599
405
|
console.log(total.value); // 200
|
|
600
406
|
price.value = 150;
|
|
601
|
-
console.log(total.value); // 300
|
|
407
|
+
console.log(total.value); // 300
|
|
602
408
|
```
|
|
603
409
|
|
|
604
|
-
|
|
410
|
+
### `effect(fn)`
|
|
605
411
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
**Use when:** You need to react to specific state changes with custom logic (different from `effect`).
|
|
412
|
+
Runs immediately and re-runs whenever its reactive dependencies change.
|
|
609
413
|
|
|
610
414
|
```js
|
|
611
|
-
import { reactive,
|
|
415
|
+
import { reactive, effect } from "vex/reactive";
|
|
612
416
|
|
|
613
417
|
const count = reactive(0);
|
|
418
|
+
const stop = effect(() => document.title = `Count: ${count.value}`);
|
|
614
419
|
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
() => count.value,
|
|
618
|
-
(newValue, oldValue, onCleanup) => {
|
|
619
|
-
console.log(`Count changed from ${oldValue} to ${newValue}`);
|
|
620
|
-
|
|
621
|
-
// Cleanup function for previous effect
|
|
622
|
-
onCleanup(() => {
|
|
623
|
-
console.log('Cleaning up previous watch effect');
|
|
624
|
-
});
|
|
625
|
-
},
|
|
626
|
-
{ immediate: false } // Run immediately on setup
|
|
627
|
-
);
|
|
628
|
-
|
|
629
|
-
count.value++; // Callback runs
|
|
420
|
+
count.value++; // effect re-runs
|
|
421
|
+
stop(); // cleanup
|
|
630
422
|
```
|
|
631
423
|
|
|
632
|
-
###
|
|
424
|
+
### `watch(source, callback)`
|
|
633
425
|
|
|
634
|
-
|
|
635
|
-
<script client>
|
|
636
|
-
import { reactive, computed, effect, watch } from ".app/reactive.js";
|
|
637
|
-
|
|
638
|
-
// Reactive state
|
|
639
|
-
const count = reactive(0);
|
|
640
|
-
const step = reactive(1);
|
|
641
|
-
|
|
642
|
-
// Computed value
|
|
643
|
-
const doubled = computed(() => count.value * 2);
|
|
644
|
-
|
|
645
|
-
// Effect for side effects
|
|
646
|
-
effect(() => {
|
|
647
|
-
console.log(`Count changed to: ${count.value}`);
|
|
648
|
-
});
|
|
649
|
-
|
|
650
|
-
// Watcher for specific logic
|
|
651
|
-
watch(
|
|
652
|
-
() => count.value,
|
|
653
|
-
(newVal, oldVal) => {
|
|
654
|
-
if (newVal > 10) {
|
|
655
|
-
console.warn('Count is getting high!');
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
);
|
|
659
|
-
|
|
660
|
-
function increment() {
|
|
661
|
-
count.value += step.value;
|
|
662
|
-
}
|
|
663
|
-
</script>
|
|
426
|
+
Runs only when the source changes (not on creation).
|
|
664
427
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
</template>
|
|
428
|
+
```js
|
|
429
|
+
import { reactive, watch } from "vex/reactive";
|
|
430
|
+
|
|
431
|
+
const count = reactive(0);
|
|
432
|
+
watch(() => count.value, (newVal, oldVal) => {
|
|
433
|
+
console.log(`${oldVal} → ${newVal}`);
|
|
434
|
+
});
|
|
673
435
|
```
|
|
674
436
|
|
|
675
|
-
### Reactivity
|
|
437
|
+
### Reactivity summary
|
|
676
438
|
|
|
677
|
-
| Function |
|
|
678
|
-
|
|
679
|
-
| `reactive()` |
|
|
680
|
-
| `effect()` |
|
|
681
|
-
| `computed()` |
|
|
682
|
-
| `watch()` |
|
|
439
|
+
| Function | Auto-runs | Returns |
|
|
440
|
+
|----------|-----------|---------|
|
|
441
|
+
| `reactive()` | No | Proxy |
|
|
442
|
+
| `effect()` | Yes (immediately + on change) | Cleanup fn |
|
|
443
|
+
| `computed()` | On dependency change | Reactive value |
|
|
444
|
+
| `watch()` | Only on change | — |
|
|
683
445
|
|
|
684
446
|
## 📝 Template Syntax
|
|
685
447
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
448
|
+
| Syntax | Description |
|
|
449
|
+
|--------|-------------|
|
|
450
|
+
| `{{expr}}` | Interpolation |
|
|
451
|
+
| `x-if="expr"` | Conditional rendering |
|
|
452
|
+
| `x-for="item in items"` | List rendering |
|
|
453
|
+
| `x-show="expr"` | Toggle `display` |
|
|
454
|
+
| `:prop="expr"` | Dynamic prop/attribute |
|
|
455
|
+
| `@click="handler"` | Event (client only) |
|
|
691
456
|
|
|
692
457
|
```html
|
|
693
458
|
<template>
|
|
694
459
|
<h1>Hello, {{name}}</h1>
|
|
695
|
-
<p>Count: {{counter}}</p>
|
|
696
|
-
</template>
|
|
697
|
-
```
|
|
698
|
-
|
|
699
|
-
### Conditionals
|
|
700
|
-
|
|
701
|
-
```html
|
|
702
|
-
<template>
|
|
703
|
-
<div v-if="isVisible">
|
|
704
|
-
This is visible
|
|
705
|
-
</div>
|
|
706
|
-
<div v-else>
|
|
707
|
-
This is hidden
|
|
708
|
-
</div>
|
|
709
|
-
</template>
|
|
710
|
-
```
|
|
711
|
-
|
|
712
|
-
### Lists
|
|
713
460
|
|
|
714
|
-
```html
|
|
715
|
-
<template>
|
|
716
461
|
<ul>
|
|
717
|
-
<li
|
|
718
|
-
{{item}}
|
|
719
|
-
</li>
|
|
462
|
+
<li x-for="item in items">{{item}}</li>
|
|
720
463
|
</ul>
|
|
721
|
-
</template>
|
|
722
|
-
```
|
|
723
464
|
|
|
724
|
-
|
|
465
|
+
<div x-if="isVisible">Visible</div>
|
|
725
466
|
|
|
726
|
-
|
|
727
|
-
<template>
|
|
728
|
-
<button @click="increment">Click me</button>
|
|
729
|
-
<input @input="handleInput" />
|
|
467
|
+
<button :disabled="count.value <= 0" @click="count.value--">-</button>
|
|
730
468
|
</template>
|
|
731
469
|
```
|
|
732
470
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
```html
|
|
736
|
-
<template>
|
|
737
|
-
<button :disabled="counter <= 0">Decrement</button>
|
|
738
|
-
<div :class="isActive ? 'active' : ''">Content</div>
|
|
739
|
-
</template>
|
|
740
|
-
```
|
|
471
|
+
> Keep logic in `getData` rather than inline expressions. Ternaries and filters are not supported in templates.
|
|
741
472
|
|
|
742
473
|
## 🛣️ Routing
|
|
743
474
|
|
|
744
|
-
###
|
|
745
|
-
|
|
746
|
-
Routes are automatically generated from the `pages/` folder structure:
|
|
747
|
-
|
|
748
|
-
```
|
|
749
|
-
pages/
|
|
750
|
-
├── page.html → /
|
|
751
|
-
├── page-ssr/page.html → /page-ssr
|
|
752
|
-
├── page-csr/page.html → /page-csr
|
|
753
|
-
└── page-ssr/[city]/page.html → /page-ssr/:city (dynamic)
|
|
754
|
-
```
|
|
475
|
+
### File-based routes
|
|
755
476
|
|
|
756
|
-
|
|
477
|
+
| File | Route |
|
|
478
|
+
|------|-------|
|
|
479
|
+
| `pages/page.vex` | `/` |
|
|
480
|
+
| `pages/about/page.vex` | `/about` |
|
|
481
|
+
| `pages/users/[id]/page.vex` | `/users/:id` |
|
|
482
|
+
| `pages/not-found/page.vex` | 404 |
|
|
483
|
+
| `pages/error/page.vex` | 500 |
|
|
757
484
|
|
|
758
|
-
|
|
485
|
+
### Dynamic routes
|
|
759
486
|
|
|
760
487
|
```html
|
|
761
|
-
<!-- pages/
|
|
488
|
+
<!-- pages/users/[id]/page.vex -->
|
|
762
489
|
<script server>
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const { city } = useRouteParams();
|
|
767
|
-
// Fetch data based on city parameter
|
|
768
|
-
return { city };
|
|
490
|
+
async function getData({ req }) {
|
|
491
|
+
const { id } = req.params;
|
|
492
|
+
return { user: await fetchUser(id) };
|
|
769
493
|
}
|
|
770
494
|
</script>
|
|
771
495
|
|
|
772
496
|
<template>
|
|
773
|
-
<h1>
|
|
497
|
+
<h1>{{user.name}}</h1>
|
|
774
498
|
</template>
|
|
775
499
|
```
|
|
776
500
|
|
|
777
|
-
###
|
|
501
|
+
### Pre-generate dynamic pages (SSG)
|
|
778
502
|
|
|
779
503
|
```js
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
504
|
+
// inside <script server>
|
|
505
|
+
export async function getStaticPaths() {
|
|
506
|
+
return [
|
|
507
|
+
{ params: { id: "1" } },
|
|
508
|
+
{ params: { id: "2" } },
|
|
509
|
+
];
|
|
510
|
+
}
|
|
784
511
|
```
|
|
785
512
|
|
|
786
|
-
###
|
|
513
|
+
### Client-side navigation
|
|
787
514
|
|
|
788
515
|
```js
|
|
789
|
-
|
|
790
|
-
import { useQueryParams } from ".app/navigation/use-query-params.js";
|
|
791
|
-
|
|
792
|
-
// Get route parameters (/page/:id)
|
|
793
|
-
const { id } = useRouteParams();
|
|
794
|
-
|
|
795
|
-
// Get query parameters (?search=query)
|
|
796
|
-
const { search } = useQueryParams();
|
|
516
|
+
window.app.navigate("/about");
|
|
797
517
|
```
|
|
798
518
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
The framework automatically prefetches pages to improve navigation performance.
|
|
519
|
+
### Route & query params (client)
|
|
802
520
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
521
|
+
```js
|
|
522
|
+
import { useRouteParams } from "vex/navigation";
|
|
523
|
+
import { useQueryParams } from "vex/navigation";
|
|
806
524
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
<nav>
|
|
810
|
-
<a href="/page-ssr" data-prefetch>SSR Page (Prefetched)</a>
|
|
811
|
-
<a href="/page-csr" data-prefetch>CSR Page (Prefetched)</a>
|
|
812
|
-
<a href="/static">Static Page (No prefetch)</a>
|
|
813
|
-
</nav>
|
|
814
|
-
</template>
|
|
525
|
+
const { id } = useRouteParams(); // reactive, updates on navigation
|
|
526
|
+
const { search } = useQueryParams();
|
|
815
527
|
```
|
|
816
528
|
|
|
817
|
-
|
|
818
|
-
1. Links with `data-prefetch` are observed using IntersectionObserver
|
|
819
|
-
2. When a link becomes visible, the page component is loaded in the background
|
|
820
|
-
3. Navigation to prefetched pages is instant (no loading delay)
|
|
821
|
-
4. Components are cached for subsequent navigations
|
|
529
|
+
## ⚡ Prefetching
|
|
822
530
|
|
|
823
|
-
|
|
824
|
-
- ⚡ Near-instant page transitions
|
|
825
|
-
- 🎯 Smart loading (only when visible)
|
|
826
|
-
- 💾 Automatic caching
|
|
827
|
-
- 🔄 Works with SPA navigation
|
|
531
|
+
Add `data-prefetch` to any `<a>` tag to prefetch the page when the link enters the viewport:
|
|
828
532
|
|
|
829
|
-
**Example in layout:**
|
|
830
533
|
```html
|
|
831
|
-
|
|
832
|
-
<template>
|
|
833
|
-
<header>
|
|
834
|
-
<nav>
|
|
835
|
-
<a href="/" data-prefetch>Home</a>
|
|
836
|
-
<a href="/page-ssr" data-prefetch>SSR</a>
|
|
837
|
-
<a href="/page-csr" data-prefetch>CSR</a>
|
|
838
|
-
<a href="/static" data-prefetch>Static</a>
|
|
839
|
-
</nav>
|
|
840
|
-
</header>
|
|
841
|
-
</template>
|
|
534
|
+
<a href="/about" data-prefetch>About</a>
|
|
842
535
|
```
|
|
843
536
|
|
|
844
|
-
|
|
537
|
+
The page component is loaded in the background; navigation to it is instant.
|
|
845
538
|
|
|
846
539
|
## 🎨 Styling
|
|
847
540
|
|
|
848
|
-
The
|
|
541
|
+
The framework uses **Tailwind CSS v4**. The dev script watches `src/input.css` and outputs to `public/styles.css`.
|
|
849
542
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
- ✅ Automatic updates
|
|
854
|
-
- ✅ Perfect for prototyping and learning
|
|
855
|
-
|
|
856
|
-
**Usage:**
|
|
857
|
-
|
|
858
|
-
```html
|
|
859
|
-
<div class="flex items-center justify-center p-4 bg-blue-500">
|
|
860
|
-
<h1 class="text-white text-2xl">Title</h1>
|
|
861
|
-
</div>
|
|
543
|
+
```css
|
|
544
|
+
/* src/input.css */
|
|
545
|
+
@import "tailwindcss";
|
|
862
546
|
```
|
|
863
547
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
During development, Tailwind CLI watches your files and compiles styles:
|
|
548
|
+
Reference the stylesheet in `root.html`:
|
|
867
549
|
|
|
868
|
-
```
|
|
869
|
-
|
|
550
|
+
```html
|
|
551
|
+
<link rel="stylesheet" href="/styles.css">
|
|
870
552
|
```
|
|
871
553
|
|
|
872
|
-
The compiled CSS is automatically generated in `.app/client/styles.css`.
|
|
873
|
-
|
|
874
554
|
## 🔧 Framework API
|
|
875
555
|
|
|
876
|
-
###
|
|
877
|
-
|
|
878
|
-
```js
|
|
879
|
-
const props = vprops({
|
|
880
|
-
userId: { required: true },
|
|
881
|
-
count: { default: 0 },
|
|
882
|
-
name: { default: "Guest" },
|
|
883
|
-
});
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
### Reactive State
|
|
887
|
-
|
|
888
|
-
```js
|
|
889
|
-
import { reactive, computed } from ".app/reactive.js";
|
|
890
|
-
|
|
891
|
-
// Reactive primitive
|
|
892
|
-
const count = reactive(0);
|
|
893
|
-
count.value++;
|
|
894
|
-
|
|
895
|
-
// Reactive object
|
|
896
|
-
const state = reactive({ name: "Alice", age: 25 });
|
|
897
|
-
state.name = "Bob";
|
|
898
|
-
|
|
899
|
-
// Computed values
|
|
900
|
-
const doubled = computed(() => count.value * 2);
|
|
901
|
-
```
|
|
902
|
-
|
|
903
|
-
### Navigation Utilities
|
|
904
|
-
|
|
905
|
-
```js
|
|
906
|
-
import { navigate } from ".app/navigation.js";
|
|
907
|
-
import { useRouteParams } from ".app/navigation/use-route-params.js";
|
|
908
|
-
import { useQueryParams } from ".app/navigation/use-query-params.js";
|
|
556
|
+
### Imports
|
|
909
557
|
|
|
910
|
-
|
|
911
|
-
|
|
558
|
+
| Import | Context | Description |
|
|
559
|
+
|--------|---------|-------------|
|
|
560
|
+
| `vex/reactive` | `<script client>` | Reactivity engine (`reactive`, `computed`, `effect`, `watch`) |
|
|
561
|
+
| `vex/navigation` | `<script client>` | Router utilities (`useRouteParams`, `useQueryParams`) |
|
|
912
562
|
|
|
913
|
-
|
|
914
|
-
const { city } = useRouteParams();
|
|
563
|
+
### Server script hooks
|
|
915
564
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
565
|
+
| Export | Description |
|
|
566
|
+
|--------|-------------|
|
|
567
|
+
| `async getData({ req, props })` | Fetches data; return value is merged into template scope |
|
|
568
|
+
| `metadata` / `async getMetadata({ req, props })` | Page-level config (`title`, `description`, `static`, `revalidate`) |
|
|
569
|
+
| `async getStaticPaths()` | Returns `[{ params }]` for pre-rendering dynamic routes |
|
|
919
570
|
|
|
920
571
|
## 📦 Available Scripts
|
|
921
572
|
|
|
922
573
|
```bash
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
pnpm biome check --write . # Format and lint with Biome
|
|
574
|
+
vex dev # Start dev server with HMR (--watch)
|
|
575
|
+
vex build # Pre-render pages, generate routes, bundle client JS
|
|
576
|
+
vex start # Production server (requires a prior build)
|
|
927
577
|
```
|
|
928
578
|
|
|
929
|
-
>
|
|
579
|
+
> `vex start` requires `vex build` to have been run first.
|
|
930
580
|
|
|
931
581
|
## 🏗️ Rendering Flow
|
|
932
582
|
|
|
933
583
|
### SSR (Server-Side Rendering)
|
|
934
584
|
|
|
935
585
|
```mermaid
|
|
936
|
-
---
|
|
937
|
-
config:
|
|
938
|
-
theme: mc
|
|
939
|
-
---
|
|
940
|
-
sequenceDiagram
|
|
941
|
-
autonumber
|
|
942
|
-
participant Client
|
|
943
|
-
participant Server
|
|
944
|
-
participant Router
|
|
945
|
-
participant ComponentProcessor
|
|
946
|
-
participant Streaming
|
|
947
|
-
participant Template
|
|
948
|
-
participant Cache
|
|
949
|
-
|
|
950
|
-
Client ->> Server: Request page (e.g., /page-ssr)
|
|
951
|
-
Server ->> Router: handlePageRequest(req, res, route)
|
|
952
|
-
Router ->> Router: Check if ISR enabled (route.meta.revalidate)
|
|
953
|
-
|
|
954
|
-
alt ISR enabled and cache valid
|
|
955
|
-
Router ->> Cache: getCachedComponentHtml(url, revalidateSeconds)
|
|
956
|
-
Cache -->> Router: Cached HTML (if not stale)
|
|
957
|
-
Router ->> Client: Send cached HTML
|
|
958
|
-
else No cache or stale
|
|
959
|
-
Router ->> ComponentProcessor: renderPageWithLayout(pagePath, context)
|
|
960
|
-
ComponentProcessor ->> ComponentProcessor: renderPage(pagePath, context)
|
|
961
|
-
ComponentProcessor ->> ComponentProcessor: processHtmlFile(filePath)
|
|
962
|
-
Note over ComponentProcessor: Extract getData, metadata,<br/>template, clientCode,<br/>serverComponents, clientComponents
|
|
963
|
-
ComponentProcessor ->> ComponentProcessor: getData(context)
|
|
964
|
-
Note over ComponentProcessor: Fetch server-side data
|
|
965
|
-
ComponentProcessor ->> Template: compileTemplateToHTML(template, data)
|
|
966
|
-
Template ->> Template: parseHTMLToNodes & processNode
|
|
967
|
-
Note over Template: Process Vue-like syntax:<br/>{{interpolation}}, v-if, v-for, etc.
|
|
968
|
-
Template -->> ComponentProcessor: Compiled HTML
|
|
969
|
-
ComponentProcessor ->> Streaming: renderComponents(html, serverComponents, clientComponents)
|
|
970
|
-
Streaming ->> Streaming: renderServerComponents(html, serverComponents)
|
|
971
|
-
Note over Streaming: Process <Suspense> boundaries<br/>Extract suspense components<br/>Render fallback content
|
|
972
|
-
Streaming ->> Streaming: renderClientComponents(html, clientComponents)
|
|
973
|
-
Note over Streaming: Replace client components with<br/>hydration templates<br/>Generate component scripts
|
|
974
|
-
Streaming -->> ComponentProcessor: { html, suspenseComponents, clientComponentsScripts }
|
|
975
|
-
ComponentProcessor ->> ComponentProcessor: generateClientScriptTags(...)
|
|
976
|
-
Note over ComponentProcessor: Generate <script> tags for<br/>client code and components
|
|
977
|
-
ComponentProcessor ->> ComponentProcessor: renderLayouts(pagePath, html, metadata)
|
|
978
|
-
Note over ComponentProcessor: Wrap page in nested layouts<br/>(innermost to outermost)<br/>Then wrap in root.html
|
|
979
|
-
ComponentProcessor -->> Router: { html, metadata, suspenseComponents, serverComponents }
|
|
980
|
-
|
|
981
|
-
alt No suspense components
|
|
982
|
-
Router ->> Client: sendResponse(res, statusCode, html)
|
|
983
|
-
Router ->> Cache: saveCachedComponentHtml (if ISR)
|
|
984
|
-
else Has suspense components (streaming)
|
|
985
|
-
Router ->> Client: sendStartStreamChunkResponse(res, html_before_closing)
|
|
986
|
-
Note over Router,Client: Stream initial HTML (before </body>)
|
|
987
|
-
loop For each suspense component
|
|
988
|
-
Router ->> Streaming: renderSuspenseComponent(suspense, serverComponents)
|
|
989
|
-
Streaming ->> Streaming: processServerComponents(content, serverComponents)
|
|
990
|
-
Note over Streaming: Render server components<br/>inside suspense boundary
|
|
991
|
-
Streaming -->> Router: Rendered HTML content
|
|
992
|
-
Router ->> Router: generateReplacementContent(suspenseId, html)
|
|
993
|
-
Note over Router: Generate <template> + hydration script
|
|
994
|
-
Router ->> Client: sendStreamChunkResponse(res, replacement_html)
|
|
995
|
-
end
|
|
996
|
-
Router ->> Client: endStreamResponse(res) - Send </body></html>
|
|
997
|
-
Router ->> Cache: saveCachedComponentHtml (if ISR and no errors)
|
|
998
|
-
end
|
|
999
|
-
end
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
### CSR (Client-Side Rendering)
|
|
1003
|
-
|
|
1004
|
-
```mermaid
|
|
1005
|
-
---
|
|
1006
|
-
config:
|
|
1007
|
-
theme: mc
|
|
1008
|
-
---
|
|
1009
586
|
sequenceDiagram
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
Navigation ->> Browser: location.href = "/account"
|
|
1042
|
-
end
|
|
1043
|
-
Navigation ->> Cache: loadRouteComponent(route.path, route.component)
|
|
1044
|
-
|
|
1045
|
-
alt Component cached
|
|
1046
|
-
Cache -->> Navigation: Cached module
|
|
1047
|
-
else Component not cached
|
|
1048
|
-
Cache ->> Component: Dynamic import(route.component)
|
|
1049
|
-
Component -->> Cache: Module with hydrateClientComponent
|
|
1050
|
-
Cache ->> Cache: routeCache.set(path, module)
|
|
1051
|
-
Cache -->> Navigation: Fresh module
|
|
1052
|
-
end
|
|
1053
|
-
|
|
1054
|
-
Navigation ->> Component: module.hydrateClientComponent(marker)
|
|
1055
|
-
Component -->> Navigation: pageNode (DOM node)
|
|
1056
|
-
|
|
1057
|
-
Navigation ->> LayoutRenderer: generate({ routeLayouts, pageNode, metadata })
|
|
1058
|
-
|
|
1059
|
-
alt Layouts already rendered
|
|
1060
|
-
LayoutRenderer ->> LayoutRenderer: getNearestRendered(routeLayouts)
|
|
1061
|
-
Note over LayoutRenderer: Find nearest cached layout
|
|
1062
|
-
LayoutRenderer ->> LayoutRenderer: getLayoutsToRender()
|
|
1063
|
-
Note over LayoutRenderer: Only render new/changed layouts
|
|
1064
|
-
else No cached layouts
|
|
1065
|
-
LayoutRenderer ->> LayoutRenderer: loadLayoutModules(layouts)
|
|
1066
|
-
Note over LayoutRenderer: Import all layout modules
|
|
1067
|
-
end
|
|
1068
|
-
|
|
1069
|
-
loop For each layout (innermost to outermost)
|
|
1070
|
-
LayoutRenderer ->> Component: layout.hydrateClientComponent(marker, { children })
|
|
1071
|
-
Component -->> LayoutRenderer: layoutNode wrapping children
|
|
1072
|
-
LayoutRenderer ->> LayoutRenderer: renderedLayouts.set(name, { node, children })
|
|
1073
|
-
end
|
|
1074
|
-
|
|
1075
|
-
LayoutRenderer -->> Navigation: { layoutId, node, metadata }
|
|
1076
|
-
|
|
1077
|
-
alt Layout already exists in DOM
|
|
1078
|
-
Navigation ->> LayoutRenderer: patch(layoutId, node)
|
|
1079
|
-
LayoutRenderer ->> Browser: Replace children in existing layout
|
|
1080
|
-
else New layout
|
|
1081
|
-
Navigation ->> Browser: root.appendChild(node)
|
|
1082
|
-
end
|
|
1083
|
-
|
|
1084
|
-
Navigation ->> Browser: addMetadata(metadata)
|
|
1085
|
-
Note over Browser: Update <title> and meta tags
|
|
1086
|
-
Navigation ->> Hydrator: hydrateComponents()
|
|
1087
|
-
|
|
1088
|
-
loop For each component marker
|
|
1089
|
-
Hydrator ->> Hydrator: Check data-hydrated attribute
|
|
1090
|
-
alt Not hydrated
|
|
1091
|
-
Hydrator ->> Component: import(`/.app/client/_components/${name}.js`)
|
|
1092
|
-
Component -->> Hydrator: Module
|
|
1093
|
-
Hydrator ->> Component: module.hydrateClientComponent(marker, props)
|
|
1094
|
-
Component -->> Hydrator: Hydrated component
|
|
1095
|
-
Hydrator ->> Hydrator: marker.dataset.hydrated = "true"
|
|
1096
|
-
end
|
|
587
|
+
participant Client
|
|
588
|
+
participant Server
|
|
589
|
+
participant Router
|
|
590
|
+
participant ComponentProcessor
|
|
591
|
+
participant Streaming
|
|
592
|
+
participant Cache
|
|
593
|
+
|
|
594
|
+
Client->>Server: GET /page
|
|
595
|
+
Server->>Router: handlePageRequest(req, res, route)
|
|
596
|
+
Router->>Router: Check ISR cache
|
|
597
|
+
|
|
598
|
+
alt Cache valid
|
|
599
|
+
Router->>Cache: getCachedHtml()
|
|
600
|
+
Cache-->>Router: html
|
|
601
|
+
Router->>Client: Send cached HTML
|
|
602
|
+
else No cache / stale
|
|
603
|
+
Router->>ComponentProcessor: renderPageWithLayout()
|
|
604
|
+
ComponentProcessor->>ComponentProcessor: processHtmlFile → getData → compileTemplate
|
|
605
|
+
ComponentProcessor->>Streaming: renderComponents (Suspense boundaries)
|
|
606
|
+
Streaming-->>ComponentProcessor: { html, suspenseComponents }
|
|
607
|
+
ComponentProcessor-->>Router: { html, suspenseComponents }
|
|
608
|
+
|
|
609
|
+
alt No Suspense
|
|
610
|
+
Router->>Client: Send full HTML
|
|
611
|
+
Router->>Cache: Save (if ISR)
|
|
612
|
+
else Has Suspense
|
|
613
|
+
Router->>Client: Stream initial HTML (skeleton)
|
|
614
|
+
loop Each suspense component
|
|
615
|
+
Router->>Streaming: renderSuspenseComponent()
|
|
616
|
+
Streaming-->>Router: Resolved HTML
|
|
617
|
+
Router->>Client: Stream replacement chunk
|
|
1097
618
|
end
|
|
619
|
+
Router->>Client: End stream
|
|
620
|
+
Router->>Cache: Save complete HTML (if ISR)
|
|
1098
621
|
end
|
|
1099
|
-
|
|
1100
|
-
Navigation ->> Navigation: currentNavigationController = null
|
|
1101
|
-
Navigation -->> User: Page rendered and interactive
|
|
622
|
+
end
|
|
1102
623
|
```
|
|
1103
624
|
|
|
1104
625
|
### ISR (Incremental Static Regeneration)
|
|
1105
626
|
|
|
1106
627
|
```mermaid
|
|
1107
|
-
---
|
|
1108
|
-
config:
|
|
1109
|
-
theme: mc
|
|
1110
|
-
---
|
|
1111
628
|
sequenceDiagram
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
participant Cache
|
|
1117
|
-
participant FileSystem
|
|
1118
|
-
participant ComponentProcessor
|
|
1119
|
-
|
|
1120
|
-
Client ->> Server: Request page (e.g., /page-ssr/madrid)
|
|
1121
|
-
Server ->> Router: handlePageRequest(req, res, route)
|
|
1122
|
-
Router ->> Router: getRevalidateSeconds(route.meta.revalidate)
|
|
1123
|
-
|
|
1124
|
-
alt revalidate = 'never' or false
|
|
1125
|
-
Note over Router: ISR disabled (Pure SSG)<br/>revalidateSeconds = -1
|
|
1126
|
-
else revalidate = number
|
|
1127
|
-
Note over Router: ISR enabled<br/>revalidateSeconds = number
|
|
1128
|
-
else revalidate = true
|
|
1129
|
-
Note over Router: ISR enabled<br/>revalidateSeconds = 60 (default)
|
|
1130
|
-
else revalidate = 0 or undefined
|
|
1131
|
-
Note over Router: No caching (SSR)<br/>revalidateSeconds = 0
|
|
1132
|
-
end
|
|
1133
|
-
|
|
1134
|
-
alt ISR enabled (revalidateSeconds !== 0)
|
|
1135
|
-
Router ->> Cache: getCachedComponentHtml(url, revalidateSeconds)
|
|
1136
|
-
Cache ->> FileSystem: getComponentHtmlDisk(componentPath)
|
|
1137
|
-
|
|
1138
|
-
alt Cache exists on disk
|
|
1139
|
-
FileSystem -->> Cache: { html, meta: { generatedAt, isStale } }
|
|
1140
|
-
Cache ->> Cache: Calculate if stale by time
|
|
1141
|
-
Note over Cache: staleByTime = (now - generatedAt) > revalidateSeconds * 1000
|
|
1142
|
-
Cache ->> Cache: isStale = meta.isStale || staleByTime
|
|
1143
|
-
Cache -->> Router: { html, isStale }
|
|
1144
|
-
|
|
1145
|
-
alt Cache valid (!isStale)
|
|
1146
|
-
Router ->> Client: sendResponse(res, 200, cachedHtml)
|
|
1147
|
-
Note over Router,Client: Instant response from cache<br/>No regeneration needed
|
|
1148
|
-
else Cache stale (isStale = true)
|
|
1149
|
-
Note over Router: Continue to regeneration<br/>Client waits for fresh content (blocking)
|
|
1150
|
-
end
|
|
1151
|
-
else No cache exists
|
|
1152
|
-
FileSystem -->> Cache: { html: null }
|
|
1153
|
-
Cache -->> Router: { html: null }
|
|
1154
|
-
Note over Router: First request - generate and cache
|
|
1155
|
-
end
|
|
1156
|
-
end
|
|
1157
|
-
|
|
1158
|
-
alt Need to regenerate (no cache, stale, or ISR disabled)
|
|
1159
|
-
Router ->> ComponentProcessor: renderPageWithLayout(pagePath, context)
|
|
1160
|
-
ComponentProcessor ->> ComponentProcessor: Process page, fetch data, render
|
|
1161
|
-
ComponentProcessor -->> Router: { html, suspenseComponents, serverComponents }
|
|
1162
|
-
|
|
1163
|
-
alt No suspense components
|
|
1164
|
-
Router ->> Client: sendResponse(res, 200, html)
|
|
1165
|
-
|
|
1166
|
-
alt ISR enabled
|
|
1167
|
-
Router ->> Cache: saveCachedComponentHtml(url, html)
|
|
1168
|
-
Cache ->> FileSystem: saveComponentHtmlDisk(componentPath, html)
|
|
1169
|
-
Note over FileSystem: Save HTML + metadata:<br/>{ generatedAt: Date.now(), isStale: false }
|
|
1170
|
-
FileSystem -->> Cache: Saved successfully
|
|
1171
|
-
Cache -->> Router: Cache updated
|
|
1172
|
-
end
|
|
1173
|
-
else Has suspense components (streaming)
|
|
1174
|
-
Router ->> Client: sendStartStreamChunkResponse(res, html_before_closing)
|
|
1175
|
-
Router ->> Router: Track htmlChunks[], abortedStream, errorStream
|
|
1176
|
-
|
|
1177
|
-
loop For each suspense component
|
|
1178
|
-
Router ->> ComponentProcessor: renderSuspenseComponent(suspense, serverComponents)
|
|
1179
|
-
ComponentProcessor -->> Router: Rendered HTML content
|
|
1180
|
-
Router ->> Router: generateReplacementContent(suspenseId, html)
|
|
1181
|
-
Router ->> Client: sendStreamChunkResponse(res, replacement_html)
|
|
1182
|
-
Router ->> Router: Append to htmlChunks[]
|
|
1183
|
-
end
|
|
1184
|
-
|
|
1185
|
-
Router ->> Client: endStreamResponse(res) - Send </body></html>
|
|
1186
|
-
|
|
1187
|
-
alt ISR enabled and no errors
|
|
1188
|
-
Router ->> Cache: saveCachedComponentHtml(url, htmlChunks.join(''))
|
|
1189
|
-
Cache ->> FileSystem: saveComponentHtmlDisk(componentPath, fullHtml)
|
|
1190
|
-
Note over FileSystem: Save complete streamed HTML<br/>with metadata
|
|
1191
|
-
FileSystem -->> Cache: Saved successfully
|
|
1192
|
-
Cache -->> Router: Cache updated for next request
|
|
1193
|
-
end
|
|
1194
|
-
end
|
|
1195
|
-
end
|
|
1196
|
-
|
|
1197
|
-
Note over Client,FileSystem: Next request within revalidation window<br/>will serve cached HTML instantly
|
|
1198
|
-
```
|
|
629
|
+
participant Client
|
|
630
|
+
participant Router
|
|
631
|
+
participant Cache
|
|
632
|
+
participant FileSystem
|
|
1199
633
|
|
|
1200
|
-
|
|
634
|
+
Client->>Router: GET /page
|
|
635
|
+
Router->>Cache: getCachedHtml(url, revalidateSeconds)
|
|
636
|
+
Cache->>FileSystem: Read HTML + meta
|
|
1201
637
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
638
|
+
alt Cache exists and valid
|
|
639
|
+
Cache-->>Router: { html, isStale: false }
|
|
640
|
+
Router->>Client: Instant cached response
|
|
641
|
+
else Cache stale or missing
|
|
642
|
+
Router->>Router: renderPageWithLayout()
|
|
643
|
+
Router->>Client: Fresh response
|
|
644
|
+
Router->>FileSystem: saveComponentHtmlDisk()
|
|
645
|
+
end
|
|
646
|
+
```
|
|
1209
647
|
|
|
1210
648
|
### Server Startup
|
|
1211
649
|
|
|
1212
|
-
Shows how `index.js` behaves differently in production vs development.
|
|
1213
|
-
|
|
1214
650
|
```mermaid
|
|
1215
|
-
---
|
|
1216
|
-
config:
|
|
1217
|
-
theme: mc
|
|
1218
|
-
---
|
|
1219
651
|
sequenceDiagram
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
participant Files
|
|
1224
|
-
participant ComponentProcessor
|
|
1225
|
-
participant RoutesFile as _routes.js (disk)
|
|
1226
|
-
|
|
1227
|
-
CLI ->> index.js: node .app/server/index.js
|
|
1228
|
-
|
|
1229
|
-
index.js ->> Files: initializeDirectories()
|
|
1230
|
-
Files -->> index.js: Directories ready (_cache/, _components/, ...)
|
|
1231
|
-
|
|
1232
|
-
alt NODE_ENV = "production"
|
|
1233
|
-
index.js ->> RoutesFile: import("./utils/_routes.js")
|
|
1234
|
-
alt _routes.js exists (pnpm build was run)
|
|
1235
|
-
RoutesFile -->> index.js: { routes }
|
|
1236
|
-
Note over index.js: Routes loaded instantly<br/>No build work done
|
|
1237
|
-
else _routes.js not found
|
|
1238
|
-
index.js ->> CLI: ERROR: Run 'pnpm build' first
|
|
1239
|
-
index.js ->> index.js: process.exit(1)
|
|
1240
|
-
end
|
|
1241
|
-
else NODE_ENV != "production" (development)
|
|
1242
|
-
index.js ->> ComponentProcessor: generateComponentsAndFillCache()
|
|
1243
|
-
Note over ComponentProcessor: Scan pages/, render static HTML,<br/>generate client JS bundles
|
|
1244
|
-
ComponentProcessor -->> index.js: Components and cache ready
|
|
1245
|
-
index.js ->> ComponentProcessor: generateRoutes()
|
|
1246
|
-
ComponentProcessor ->> RoutesFile: Write server _routes.js
|
|
1247
|
-
ComponentProcessor ->> RoutesFile: Write client _routes.js
|
|
1248
|
-
ComponentProcessor -->> index.js: { serverRoutes }
|
|
1249
|
-
end
|
|
652
|
+
participant CLI
|
|
653
|
+
participant index.js
|
|
654
|
+
participant ComponentProcessor
|
|
1250
655
|
|
|
1251
|
-
|
|
1252
|
-
index.js ->> index.js: app.listen(PORT)
|
|
1253
|
-
index.js -->> CLI: Server running on port 3000
|
|
1254
|
-
```
|
|
1255
|
-
|
|
1256
|
-
### Build Process
|
|
1257
|
-
|
|
1258
|
-
Shows what `pnpm build` does step by step.
|
|
1259
|
-
|
|
1260
|
-
```mermaid
|
|
1261
|
-
---
|
|
1262
|
-
config:
|
|
1263
|
-
theme: mc
|
|
1264
|
-
---
|
|
1265
|
-
flowchart TD
|
|
1266
|
-
A([pnpm build]) --> B[node .app/server/prebuild.js]
|
|
1267
|
-
B --> C[initializeDirectories\nCreate _cache/, _components/]
|
|
1268
|
-
C --> D[generateComponentsAndFillCache]
|
|
1269
|
-
|
|
1270
|
-
D --> E[getPageFiles with layouts=true]
|
|
1271
|
-
E --> F[For each .html file in pages/]
|
|
1272
|
-
|
|
1273
|
-
F --> G[processHtmlFile\nExtract server script, client script, template]
|
|
1274
|
-
G --> H[Execute server script\nget getData, getMetadata, getStaticPaths]
|
|
1275
|
-
H --> I{Has getStaticPaths?}
|
|
1276
|
-
|
|
1277
|
-
I -->|Yes| J[getStaticPaths\nreturns array of params]
|
|
1278
|
-
I -->|No| K[Single path with empty params]
|
|
1279
|
-
|
|
1280
|
-
J --> L[For each param set:\nrenderHtmlFile with req.params]
|
|
1281
|
-
K --> L
|
|
1282
|
-
|
|
1283
|
-
L --> M{canCSR?\nneverRevalidate AND no server components\nAND no getData}
|
|
1284
|
-
M -->|Yes - CSR page| N[saveClientComponent\nGenerate .js bundle in _components/]
|
|
1285
|
-
M -->|No - SSR/ISR/SSG page| O[saveComponentHtmlDisk\nSave rendered HTML to _cache/]
|
|
1286
|
-
|
|
1287
|
-
N --> P{Has nested server\nor client components?}
|
|
1288
|
-
O --> P
|
|
1289
|
-
P -->|Yes| Q[Recursively process\neach referenced component]
|
|
1290
|
-
P -->|No| R[Page done]
|
|
1291
|
-
Q --> R
|
|
1292
|
-
|
|
1293
|
-
R --> S[generateRoutes]
|
|
1294
|
-
S --> T[getPageFiles without layouts]
|
|
1295
|
-
T --> U[For each page: getRouteFileData\nResolve canCSR, metadata, static paths]
|
|
1296
|
-
U --> V[saveServerRoutesFile\n.app/server/utils/_routes.js]
|
|
1297
|
-
U --> W[saveClientRoutesFile\n.app/client/services/_routes.js]
|
|
1298
|
-
|
|
1299
|
-
V --> X[Tailwind CSS minify\n_input.css → styles.css]
|
|
1300
|
-
W --> X
|
|
1301
|
-
X --> Y([Build complete ✅])
|
|
1302
|
-
```
|
|
1303
|
-
|
|
1304
|
-
**`canCSR` logic:** A page is treated as CSR (client-only bundle, no server HTML) when all three conditions are true:
|
|
1305
|
-
- `revalidate` is `false` or `"never"` (i.e. never needs server refresh)
|
|
1306
|
-
- No server components (`<script server>`) in the tree
|
|
1307
|
-
- No `getData` function
|
|
1308
|
-
|
|
1309
|
-
Otherwise the page is SSR/ISR/SSG and its HTML is pre-rendered into `_cache/`.
|
|
656
|
+
CLI->>index.js: node .../server/index.js
|
|
1310
657
|
|
|
1311
|
-
|
|
658
|
+
alt NODE_ENV=production
|
|
659
|
+
index.js->>index.js: import .vexjs/_routes.js
|
|
660
|
+
note right of index.js: Fails if pnpm build not run
|
|
661
|
+
else development
|
|
662
|
+
index.js->>ComponentProcessor: build()
|
|
663
|
+
ComponentProcessor-->>index.js: serverRoutes
|
|
664
|
+
end
|
|
1312
665
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
```mermaid
|
|
1316
|
-
---
|
|
1317
|
-
config:
|
|
1318
|
-
theme: mc
|
|
1319
|
-
---
|
|
1320
|
-
sequenceDiagram
|
|
1321
|
-
autonumber
|
|
1322
|
-
participant HTML as Browser DOM
|
|
1323
|
-
participant Script as hydrate-client-components.js
|
|
1324
|
-
participant Observer as MutationObserver
|
|
1325
|
-
participant Module as _components/{name}.js
|
|
1326
|
-
|
|
1327
|
-
HTML ->> Script: <script src="..."> loads (IIFE, non-module)
|
|
1328
|
-
Script ->> Observer: observe(document, childList + subtree)
|
|
1329
|
-
Note over Observer: Watches for nodes added by SSR streaming
|
|
1330
|
-
|
|
1331
|
-
alt document.readyState = "loading"
|
|
1332
|
-
HTML -->> Script: DOMContentLoaded event fires
|
|
1333
|
-
Script ->> Script: hydrateComponents(document)
|
|
1334
|
-
Script ->> Observer: disconnect()
|
|
1335
|
-
Note over Observer: Streaming content already parsed,<br/>observer no longer needed
|
|
1336
|
-
else document already interactive
|
|
1337
|
-
Script ->> Script: hydrateComponents(document) immediately
|
|
1338
|
-
end
|
|
1339
|
-
|
|
1340
|
-
loop For each [data-client:component]:not([data-hydrated="true"])
|
|
1341
|
-
Script ->> Script: Read data-client:component (component name/hash)
|
|
1342
|
-
Script ->> Script: JSON.parse(data-client:props)
|
|
1343
|
-
Script ->> Module: import(/.app/client/_components/${name}.js)
|
|
1344
|
-
Module -->> Script: { hydrateClientComponent }
|
|
1345
|
-
Script ->> Module: hydrateClientComponent(marker, props)
|
|
1346
|
-
Note over Module: Replaces <template> marker with<br/>reactive DOM, binds events
|
|
1347
|
-
Script ->> Script: marker.dataset.hydrated = "true"
|
|
1348
|
-
end
|
|
1349
|
-
|
|
1350
|
-
Note over Script: window.hydrateComponents exposed globally<br/>Called by SPA navigation after each route change
|
|
666
|
+
index.js->>index.js: registerSSRRoutes(app, serverRoutes)
|
|
667
|
+
index.js->>CLI: Listening on :3001
|
|
1351
668
|
```
|
|
1352
669
|
|
|
1353
|
-
**Key detail:** The `<script>` tag that loads this file has no `type="module"` — it's an IIFE that runs synchronously and exposes `window.hydrateComponents` for the SPA router to call after each navigation.
|
|
1354
|
-
|
|
1355
670
|
## 🗺️ Roadmap
|
|
1356
671
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
- [x]
|
|
1360
|
-
- [x]
|
|
1361
|
-
- [x]
|
|
1362
|
-
- [x]
|
|
1363
|
-
- [x]
|
|
1364
|
-
- [x]
|
|
1365
|
-
- [x]
|
|
1366
|
-
- [x]
|
|
1367
|
-
- [x]
|
|
1368
|
-
- [x]
|
|
1369
|
-
- [x]
|
|
1370
|
-
- [x]
|
|
1371
|
-
- [x]
|
|
1372
|
-
- [x]
|
|
1373
|
-
- [x]
|
|
1374
|
-
- [
|
|
1375
|
-
- [
|
|
1376
|
-
- [ ]
|
|
1377
|
-
- [ ] **Change syntax**
|
|
1378
|
-
- [ ] **Create NPM extension package** - Extension to recognize sintax
|
|
1379
|
-
- [ ] **Use custom extension** - custom extension to have correct imports, lint, colors, etc.
|
|
1380
|
-
- [ ] **Create NPM package** - Create package to save all logic framework and reused it in other projects
|
|
1381
|
-
- [ ] **Cache with CDN**
|
|
1382
|
-
- [ ] **Fix error replace marker** Only occurs when template has multiple childs no wrapped in div /fragment
|
|
1383
|
-
- [ ] **Authentication** - Built-in authentication system with middleware support
|
|
672
|
+
- [x] File-based routing with dynamic segments
|
|
673
|
+
- [x] SSR / CSR / SSG / ISR rendering strategies
|
|
674
|
+
- [x] Incremental Static Regeneration with background revalidation
|
|
675
|
+
- [x] Static path pre-generation (`getStaticPaths`)
|
|
676
|
+
- [x] Auto-generated server and client route registries
|
|
677
|
+
- [x] Streaming Suspense with fallback UI
|
|
678
|
+
- [x] Vue-like reactive system (`reactive`, `computed`, `effect`, `watch`)
|
|
679
|
+
- [x] Nested layouts per route
|
|
680
|
+
- [x] SPA client-side navigation
|
|
681
|
+
- [x] Prefetching with IntersectionObserver
|
|
682
|
+
- [x] Server-side data caching (`withCache`)
|
|
683
|
+
- [x] HMR (hot reload) in development
|
|
684
|
+
- [x] Component props (`xprops`)
|
|
685
|
+
- [x] `vex/` import prefix for framework utilities
|
|
686
|
+
- [x] `vex.config.json` — configurable `srcDir` and `watchIgnore`
|
|
687
|
+
- [x] Published to npm as `@cfdez11/vex`
|
|
688
|
+
- [x] VS Code extension with syntax highlighting and go-to-definition
|
|
689
|
+
- [ ] Authentication middleware
|
|
690
|
+
- [ ] CDN cache integration
|
|
691
|
+
- [ ] Fix Suspense marker replacement with multi-root templates
|