@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 CHANGED
@@ -1,1383 +1,691 @@
1
- # Vanilla JS Framework
1
+ # @cfdez11/vex
2
2
 
3
- [![pnpm](https://img.shields.io/badge/pnpm-F69220?logo=pnpm&logoColor=fff)](#)
3
+ [![npm](https://img.shields.io/npm/v/@cfdez11/vex)](https://www.npmjs.com/package/@cfdez11/vex)
4
4
  [![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?logo=javascript&logoColor=000)](#)
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-6DA55F?logo=node.js&logoColor=white)](#)
6
6
 
7
- A minimalist vanilla JavaScript framework with support for Server-Side Rendering (SSR), Client-Side Rendering (CSR), reactive components, and streaming with suspense.
8
-
9
- ## Table of Contents
10
-
11
- - [Vanilla JS Framework](#vanilla-js-framework)
12
- - [ Table of Contents](#-table-of-contents)
13
- - [ Key Features](#-key-features)
14
- - [�📁 Project Structure](#-project-structure)
15
- - [🚀 Quick Start](#-quick-start)
16
- - [📄 Creating a Page](#-creating-a-page)
17
- - [🧩 Components](#-components)
18
- - [Component Structure](#component-structure)
19
- - [Server Components](#server-components)
20
- - [Using Components](#using-components)
21
- - [🎭 Rendering Strategies](#-rendering-strategies)
22
- - [SSR - Server-Side Rendering](#ssr---server-side-rendering)
23
- - [CSR - Client-Side Rendering](#csr---client-side-rendering)
24
- - [SSG - Static Site Generation](#ssg---static-site-generation)
25
- - [ISR - Incremental Static Regeneration](#isr---incremental-static-regeneration)
26
- - [📐 Layouts](#-layouts)
27
- - [Root Layout](#root-layout)
28
- - [Custom Nested Layouts](#custom-nested-layouts)
29
- - [⏳ Suspense (Streaming)](#-suspense-streaming)
30
- - [🔄 Reactive System](#-reactive-system)
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
- ├── pages/ # Application pages
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
- # Install dependencies
146
- pnpm install
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
- # Start development server
149
- pnpm dev
74
+ ## 📁 Project Structure
150
75
 
151
- # Start production server
152
- pnpm start
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
- The server will be available at `http://localhost:3000`
95
+ > Generated files are written to `.vexjs/` — do not edit them manually.
156
96
 
157
- ## 📄 Creating a Page
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
- Pages are created in `pages/` with the following structure:
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.html -->
129
+ <!-- pages/example/page.vex -->
163
130
  <script server>
164
- // Server-side imports (components)
165
- import UserCard from "components/user-card.html";
131
+ import UserCard from "components/user-card.vex";
166
132
 
167
- // Server-side data fetching
168
- async function getData() {
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
- // Client-side component imports
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="123" />
147
+ <UserCard :userId="1" />
188
148
  </template>
189
149
  ```
190
150
 
191
- **Routes are auto-generated** from the `pages/` folder structure. No need to manually register routes!
151
+ Routes are auto-generated from the `pages/` folder no manual registration needed.
192
152
 
193
153
  ## 🧩 Components
194
154
 
195
- Components are defined in `.html` files within the `components/` folder. They can be either client-side or server-side components.
155
+ Components are `.vex` files. They can live in any folder; the default convention is `components/`.
196
156
 
197
- ### Component Structure
157
+ ### Component structure
198
158
 
199
159
  ```html
200
- <!-- components/counter.html -->
160
+ <!-- components/counter.vex -->
201
161
  <script client>
202
- import { reactive, computed } from ".app/reactive.js";
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
- // Computed values
222
- const stars = computed(() => Array.from({ length: counter.value }, () => "⭐"));
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 class="flex items-center gap-4">
227
- <button @click="decrement" :disabled="counter <= 0">
228
- Sub
229
- </button>
230
- <span>{{counter}}</span>
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 Components
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.html -->
182
+ <!-- components/user-card.vex -->
245
183
  <script server>
246
- const props = vprops({
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(res => res.json());
188
+ .then(r => r.json());
253
189
  return { user };
254
190
  }
255
191
  </script>
256
192
 
257
193
  <template>
258
- <div class="user-card">
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 Components
201
+ ### Using components
266
202
 
267
- Import and use components in your pages:
203
+ Import them in any page or component:
268
204
 
269
205
  ```html
270
- <!-- In pages/page.html -->
271
- <script client>
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 server>
276
- import UserCard from "components/user-card.html";
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="123" />
215
+ <Counter :start="5" />
216
+ <UserCard :userId="1" />
282
217
  </template>
283
218
  ```
284
219
 
285
- ## 🎭 Rendering Strategies
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
- The framework supports multiple rendering strategies to optimize performance and user experience based on your needs.
229
+ Pass them from the parent template:
288
230
 
289
- ### SSR - Server-Side Rendering
231
+ ```html
232
+ <UserCard :userId="user.id" label="Profile" />
233
+ ```
234
+
235
+ ## 🎭 Rendering Strategies
290
236
 
291
- **When to use**: Dynamic content that changes frequently, SEO-critical pages, personalized content.
237
+ Configured via `metadata` in `<script server>`.
292
238
 
293
- Pages are rendered on the server for each request. HTML is generated with fresh data and sent to the client.
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
- // Fresh data on every request
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
- **Characteristics:**
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
- **When to use**: Highly interactive dashboards, authenticated areas, apps with frequent updates.
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 ".app/reactive.js";
332
-
264
+ import { reactive } from "vex/reactive";
265
+
333
266
  const data = reactive(null);
334
-
335
- // Fetch data on client
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 v-if="data">
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
- **Characteristics:**
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
- **When to use**: Content that rarely changes (docs, blogs, marketing pages).
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
- // Fetched once at build time
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
- **Characteristics:**
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
- Pages are statically generated but automatically regenerate after a specified time period.
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
- description: "Weather with ISR",
416
- revalidate: 10 // Regenerate every 10 seconds
301
+ revalidate: 60, // regenerate every 60 s
417
302
  };
418
- </script>
419
303
 
420
- <template>
421
- <h1>Weather in {{city}}</h1>
422
- <p>Temperature: {{weather.temp}}°C</p>
423
- <p class="text-sm text-gray-500">Updates every 10 seconds</p>
424
- </template>
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
- **Characteristics:**
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
- **Revalidation options:**
435
- ```js
436
- const metadata = {
437
- revalidate: 'never', // Pure static (SSG)
438
- revalidate: 10, // Regenerate every 10 seconds (ISR)
439
- // No revalidate // Server-side rendering on every request (SSR)
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
- The framework supports nested layouts for consistent page structure.
324
+ ### Root layout
446
325
 
447
- ### Root Layout
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
- <!-- pages/layout.html -->
453
- <script client>
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
- <div>
461
- <header class="bg-white shadow">
462
- <nav>
463
- <a href="/">Home</a>
464
- <a href="/page-ssr">SSR</a>
465
- <a href="/page-csr">CSR</a>
466
- </nav>
467
- </header>
468
-
469
- <main>
470
- {{children}} <!-- Page content injected here -->
471
- </main>
472
-
473
- <footer class="bg-gray-800 text-white">
474
- <p>&copy; 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
- ### Custom Nested Layouts
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
- <template>
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/layout.html (root layout)
507
- └─> pages/static/layout.html (custom layout for /static/*)
508
- └─> pages/static/page.html (page content)
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
- Allows showing a fallback while content loads asynchronously:
360
+ Streams a fallback immediately while a slow component loads:
514
361
 
515
362
  ```html
516
363
  <script server>
517
- import UserCardDelayed from "components/user-card-delayed.html";
518
- import UserCardSkeleton from "components/user-card-skeleton.html";
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="<UserCardSkeleton />">
523
- <UserCardDelayed userId="123" />
369
+ <Suspense :fallback="<SkeletonCard />">
370
+ <SlowCard :userId="1" />
524
371
  </Suspense>
525
372
  </template>
526
373
  ```
527
374
 
528
- **Benefits:**
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
- The reactive system provides a Vue-like reactivity API with automatic dependency tracking and UI updates.
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
- **Use when:** You need reactive state that automatically updates the UI.
381
+ ### `reactive(value)`
546
382
 
547
383
  ```js
548
- import { reactive } from ".app/reactive.js";
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
- // Effect runs immediately and on every count change
573
- const cleanup = effect(() => {
574
- console.log(`Count is: ${count.value}`);
575
- document.title = `Count: ${count.value}`;
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
- #### `computed(getter)`
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 ".app/reactive.js";
399
+ import { reactive, computed } from "vex/reactive";
592
400
 
593
401
  const price = reactive(100);
594
- const quantity = reactive(2);
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 (automatically recalculated)
407
+ console.log(total.value); // 300
602
408
  ```
603
409
 
604
- #### `watch(source, callback, options)`
410
+ ### `effect(fn)`
605
411
 
606
- Watches a reactive source and runs a callback when its value changes.
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, watch } from ".app/reactive.js";
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
- // Watch runs only when count changes (not immediately)
616
- watch(
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
- ### In Components
424
+ ### `watch(source, callback)`
633
425
 
634
- ```html
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
- <template>
666
- <div>
667
- <p>Count: {{count}}</p>
668
- <p>Doubled: {{doubled}}</p>
669
- <p>Step: {{step}}</p>
670
- <button @click="increment">Increment by {{step}}</button>
671
- </div>
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 Comparison
437
+ ### Reactivity summary
676
438
 
677
- | Function | When to Use | Auto-runs | Returns |
678
- |----------|-------------|-----------|----------|
679
- | `reactive()` | Create reactive state | No | Proxy object |
680
- | `effect()` | Side effects (logging, DOM, etc.) | Yes (immediately + on changes) | Cleanup function |
681
- | `computed()` | Derived/calculated values | Yes (on dependency change) | Reactive value |
682
- | `watch()` | React to specific changes | Optional (with `immediate`) | Nothing |
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
- Components use a template syntax that supports interpolation, directives, and event handlers.
687
-
688
- > **Note:** The template syntax is inspired by Vue.js for educational purposes. This framework was created as a learning exercise to understand how modern frameworks work internally while practicing Vue.js concepts.
689
-
690
- ### Interpolation
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 v-for="item in items">
718
- {{item}}
719
- </li>
462
+ <li x-for="item in items">{{item}}</li>
720
463
  </ul>
721
- </template>
722
- ```
723
464
 
724
- ### Event Handlers
465
+ <div x-if="isVisible">Visible</div>
725
466
 
726
- ```html
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
- ### Attributes
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
- ### Auto-Generated Routes
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
- ### Dynamic Routes
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
- Create dynamic routes using `[param]` syntax:
485
+ ### Dynamic routes
759
486
 
760
487
  ```html
761
- <!-- pages/page-ssr/[city]/page.html -->
488
+ <!-- pages/users/[id]/page.vex -->
762
489
  <script server>
763
- import { useRouteParams } from ".app/navigation/use-route-params.js";
764
-
765
- async function getData() {
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>Weather for {{city}}</h1>
497
+ <h1>{{user.name}}</h1>
774
498
  </template>
775
499
  ```
776
500
 
777
- ### Client-Side Navigation
501
+ ### Pre-generate dynamic pages (SSG)
778
502
 
779
503
  ```js
780
- import { navigate } from ".app/navigation.js";
781
-
782
- // Navigate without page reload
783
- navigate("/page-ssr");
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
- ### Accessing Route Parameters
513
+ ### Client-side navigation
787
514
 
788
515
  ```js
789
- import { useRouteParams } from ".app/navigation/use-route-params.js";
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
- ## Prefetching
800
-
801
- The framework automatically prefetches pages to improve navigation performance.
519
+ ### Route & query params (client)
802
520
 
803
- ### Automatic Prefetching
804
-
805
- Add the `data-prefetch` attribute to any link to prefetch the page when it enters the viewport:
521
+ ```js
522
+ import { useRouteParams } from "vex/navigation";
523
+ import { useQueryParams } from "vex/navigation";
806
524
 
807
- ```html
808
- <template>
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
- **How it works:**
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
- **Benefits:**
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
- <!-- pages/layout.html -->
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
- **Performance tip:** Use prefetching for frequently accessed pages or important navigation paths.
537
+ The page component is loaded in the background; navigation to it is instant.
845
538
 
846
539
  ## 🎨 Styling
847
540
 
848
- The project uses **Tailwind CSS v4** via CDN for simplicity and zero configuration.
541
+ The framework uses **Tailwind CSS v4**. The dev script watches `src/input.css` and outputs to `public/styles.css`.
849
542
 
850
- **Why CDN?**
851
- - No build step required
852
- - ✅ Instant setup
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
- **Compilation:**
865
-
866
- During development, Tailwind CLI watches your files and compiles styles:
548
+ Reference the stylesheet in `root.html`:
867
549
 
868
- ```bash
869
- pnpm dev # Runs Tailwind in watch mode + server
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
- ### Component Props
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
- // Navigate to a route
911
- navigate("/page-ssr/madrid");
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
- // Access route params
914
- const { city } = useRouteParams();
563
+ ### Server script hooks
915
564
 
916
- // Access query params
917
- const { search } = useQueryParams();
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
- pnpm dev # Development server: builds + watches (auto-reloads on changes)
924
- pnpm build # Production build: generates routes, client bundles and minifies CSS
925
- pnpm start # Production server: requires a prior pnpm build
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
- > ⚠️ `pnpm start` requires `pnpm build` to have been run first. In production the server loads pre-built routes from `_routes.js` without executing the build pipeline.
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
- autonumber
1011
- participant User
1012
- participant Browser
1013
- participant Navigation
1014
- participant Router
1015
- participant Cache
1016
- participant LayoutRenderer
1017
- participant Component
1018
- participant Hydrator
1019
-
1020
- User ->> Browser: Click link or initial page load
1021
- Browser ->> Navigation: navigate(path)
1022
- Navigation ->> Navigation: abortPrevious()
1023
- Note over Navigation: Cancel any in-progress navigation
1024
- Navigation ->> Router: findRouteWithParams(path)
1025
- Router -->> Navigation: { route, params }
1026
- Navigation ->> Navigation: updateRouteParams(params)
1027
- Navigation ->> Navigation: history.pushState({}, "", path)
1028
-
1029
- alt Route is SSR (meta.ssr = true)
1030
- Navigation ->> Navigation: layoutRenderer.reset()
1031
- Navigation ->> Browser: fetch(path, { signal })
1032
- Browser -->> Navigation: Response stream
1033
- Navigation ->> Navigation: renderSSRPage(path, signal)
1034
- Note over Navigation: Progressive rendering:<br/>- Parse <main><br/>- Parse <template><br/>- Execute <script><br/>- Update metadata
1035
- Navigation ->> Hydrator: hydrateComponents()
1036
- Note over Hydrator: Hydrate streamed components
1037
- else Route is CSR (meta.ssr = false)
1038
- alt route.meta.requiresAuth && !app.Store.loggedIn
1039
- Navigation ->> Browser: location.href = "/account/login"
1040
- else route.meta.guestOnly && app.Store.loggedIn
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
- autonumber
1113
- participant Client
1114
- participant Server
1115
- participant Router
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
- **ISR Flow Summary:**
634
+ Client->>Router: GET /page
635
+ Router->>Cache: getCachedHtml(url, revalidateSeconds)
636
+ Cache->>FileSystem: Read HTML + meta
1201
637
 
1202
- 1. **Cache Check**: Verifies if cached HTML exists and if it's still valid
1203
- 2. **Stale Detection**: Compares current time vs. generation time + revalidation seconds
1204
- 3. **Instant Serve**: If cache is valid, serves immediately without regeneration
1205
- 4. **Regeneration**: If cache is stale or missing, regenerates the page
1206
- 5. **Background Save**: After regeneration, saves to cache for future requests
1207
- 6. **Streaming Support**: Handles suspense components and saves complete HTML
1208
- 7. **Error Handling**: Prevents caching if errors occur during streaming
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
- autonumber
1221
- participant CLI
1222
- participant index.js
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
- index.js ->> index.js: registerSSRRoutes(app, serverRoutes)
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
- ### Client Hydration
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
- Shows how `hydrate-client-components.js` mounts interactive components after the HTML is in the DOM.
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
- Future features planned for implementation:
1358
-
1359
- - [x] **Language sintaxis** - Unify sintaxis to use same conditional tags, lists, etc in server and client.
1360
- - [x] **Inject client component script** - Use other technique to inject js in client, to avoid have scripts in html, check Nextjs, svelte, vue
1361
- - [x] **Metadata dynamic** - Optional add export func if the user wants to fetch. Also this func can receive the result of getData to not repeat the same fetch
1362
- - [x] **Add dynamic pages CSR and SSR** - Add dynamic routes
1363
- - [x] **Incremental Static Regeneration / Static Pages** - Regenerate static pages on-demand / never
1364
- - [x] **Generate Static Params** - Pre-generate pages with dynamic routes at build time
1365
- - [x] **Auto generated routes** - Auto generate server and client routes
1366
- - [x] **Unify fs methods** - Same constants files, unify fs functions in files, unify comments, etc
1367
- - [x] **Cache getData** - Implement caching layer for data fetching functions
1368
- - [x] **Cache Server Pages** - Cache rendered server pages for improved performance
1369
- - [x] **Link Component** - Custom link component with prefetching capabilities
1370
- - [x] **Prefetch Pages** - Automatically prefetch pages on link hover/visibility
1371
- - [x] **Restructure Directories** - Optimize project structure and organization
1372
- - [x] **Auto-generated Files** - Automatic generation of routes, utility files, and configurations based on code and pages directories
1373
- - [x] **Auto-generated Components** - Automatic generation components only based on pages imports
1374
- - [x] **Optimize auto generated routes**
1375
- - [x] **Layouts** - Layouts inside sub routes
1376
- - [ ] **Regeneration in background** - Regenerate page after send response (locks)
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