@cfdez11/vex 0.10.4 → 0.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -642
- package/dist/server/prebuild.js +1 -1
- package/dist/server/utils/files.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,26 +6,7 @@
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
-
|
|
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)
|
|
9
|
+
Requires **Node.js >= 18**.
|
|
29
10
|
|
|
30
11
|
## Installation
|
|
31
12
|
|
|
@@ -33,9 +14,7 @@ A vanilla JavaScript meta-framework built on Express.js with file-based routing,
|
|
|
33
14
|
npm install @cfdez11/vex
|
|
34
15
|
```
|
|
35
16
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
## 🚀 Quick Start
|
|
17
|
+
## Quick Start
|
|
39
18
|
|
|
40
19
|
```bash
|
|
41
20
|
mkdir my-app && cd my-app
|
|
@@ -64,621 +43,21 @@ Create the minimum structure:
|
|
|
64
43
|
```bash
|
|
65
44
|
mkdir -p pages src public
|
|
66
45
|
echo '@import "tailwindcss";' > src/input.css
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
46
|
npm run dev
|
|
71
47
|
# → http://localhost:3001
|
|
72
48
|
```
|
|
73
49
|
|
|
74
|
-
##
|
|
75
|
-
|
|
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)
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
> Generated files are written to `.vexjs/` — do not edit them manually.
|
|
96
|
-
|
|
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.
|
|
113
|
-
|
|
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
|
|
127
|
-
|
|
128
|
-
```html
|
|
129
|
-
<!-- pages/example/page.vex -->
|
|
130
|
-
<script server>
|
|
131
|
-
import UserCard from "@/components/user-card.vex";
|
|
132
|
-
|
|
133
|
-
const metadata = { title: "My Page", description: "Page description" };
|
|
134
|
-
|
|
135
|
-
async function getData({ req }) {
|
|
136
|
-
return { message: "Hello from the server" };
|
|
137
|
-
}
|
|
138
|
-
</script>
|
|
139
|
-
|
|
140
|
-
<script client>
|
|
141
|
-
import Counter from "@/components/counter.vex";
|
|
142
|
-
</script>
|
|
143
|
-
|
|
144
|
-
<template>
|
|
145
|
-
<h1>{{message}}</h1>
|
|
146
|
-
<Counter start="0" />
|
|
147
|
-
<UserCard :userId="1" />
|
|
148
|
-
</template>
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
Routes are auto-generated from the `pages/` folder — no manual registration needed.
|
|
152
|
-
|
|
153
|
-
## 🧩 Components
|
|
154
|
-
|
|
155
|
-
Components are `.vex` files. They can live in any folder; the default convention is `components/`.
|
|
156
|
-
|
|
157
|
-
### Component structure
|
|
158
|
-
|
|
159
|
-
```html
|
|
160
|
-
<!-- components/counter.vex -->
|
|
161
|
-
<script client>
|
|
162
|
-
import { reactive, computed } from "vex/reactive";
|
|
163
|
-
|
|
164
|
-
const props = xprops({ start: { default: 0 } });
|
|
165
|
-
const count = reactive(props.start);
|
|
166
|
-
const stars = computed(() => "⭐".repeat(count.value));
|
|
167
|
-
</script>
|
|
168
|
-
|
|
169
|
-
<template>
|
|
170
|
-
<div>
|
|
171
|
-
<button @click="count.value--">-</button>
|
|
172
|
-
<span>{{count.value}}</span>
|
|
173
|
-
<button @click="count.value++">+</button>
|
|
174
|
-
<div>{{stars.value}}</div>
|
|
175
|
-
</div>
|
|
176
|
-
</template>
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Server components
|
|
180
|
-
|
|
181
|
-
```html
|
|
182
|
-
<!-- components/user-card.vex -->
|
|
183
|
-
<script server>
|
|
184
|
-
const props = xprops({ userId: { default: null } });
|
|
185
|
-
|
|
186
|
-
async function getData({ props }) {
|
|
187
|
-
const user = await fetch(`https://api.example.com/users/${props.userId}`)
|
|
188
|
-
.then(r => r.json());
|
|
189
|
-
return { user };
|
|
190
|
-
}
|
|
191
|
-
</script>
|
|
192
|
-
|
|
193
|
-
<template>
|
|
194
|
-
<div>
|
|
195
|
-
<h3>{{user.name}}</h3>
|
|
196
|
-
<p>{{user.email}}</p>
|
|
197
|
-
</div>
|
|
198
|
-
</template>
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### Using components
|
|
202
|
-
|
|
203
|
-
Import them in any page or component:
|
|
204
|
-
|
|
205
|
-
```html
|
|
206
|
-
<script server>
|
|
207
|
-
import UserCard from "@/components/user-card.vex";
|
|
208
|
-
</script>
|
|
209
|
-
|
|
210
|
-
<script client>
|
|
211
|
-
import Counter from "@/components/counter.vex";
|
|
212
|
-
</script>
|
|
213
|
-
|
|
214
|
-
<template>
|
|
215
|
-
<Counter :start="5" />
|
|
216
|
-
<UserCard :userId="1" />
|
|
217
|
-
</template>
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Component props (`xprops`)
|
|
221
|
-
|
|
222
|
-
```js
|
|
223
|
-
const props = xprops({
|
|
224
|
-
userId: { default: null },
|
|
225
|
-
label: { default: "Click me" },
|
|
226
|
-
});
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Pass them from the parent template:
|
|
230
|
-
|
|
231
|
-
```html
|
|
232
|
-
<UserCard :userId="user.id" label="Profile" />
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
## 🎭 Rendering Strategies
|
|
236
|
-
|
|
237
|
-
Configured via `metadata` in `<script server>`.
|
|
238
|
-
|
|
239
|
-
### SSR — Server-Side Rendering (default)
|
|
240
|
-
|
|
241
|
-
Rendered fresh on every request. Best for dynamic, personalised or SEO-critical pages.
|
|
242
|
-
|
|
243
|
-
```html
|
|
244
|
-
<script server>
|
|
245
|
-
const metadata = { title: "Live Data" };
|
|
246
|
-
|
|
247
|
-
async function getData() {
|
|
248
|
-
const data = await fetch("https://api.example.com/data").then(r => r.json());
|
|
249
|
-
return { data };
|
|
250
|
-
}
|
|
251
|
-
</script>
|
|
252
|
-
|
|
253
|
-
<template>
|
|
254
|
-
<h1>{{data.title}}</h1>
|
|
255
|
-
</template>
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
### CSR — Client-Side Rendering
|
|
259
|
-
|
|
260
|
-
No server-rendered HTML. The page fetches its own data in the browser. Use for highly interactive or authenticated areas.
|
|
261
|
-
|
|
262
|
-
```html
|
|
263
|
-
<script client>
|
|
264
|
-
import { reactive } from "vex/reactive";
|
|
265
|
-
|
|
266
|
-
const data = reactive(null);
|
|
267
|
-
|
|
268
|
-
fetch("/api/data").then(r => r.json()).then(v => data.value = v);
|
|
269
|
-
</script>
|
|
270
|
-
|
|
271
|
-
<template>
|
|
272
|
-
<div x-if="data.value">
|
|
273
|
-
<h1>{{data.value.title}}</h1>
|
|
274
|
-
</div>
|
|
275
|
-
<div x-if="!data.value">Loading…</div>
|
|
276
|
-
</template>
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### SSG — Static Site Generation
|
|
280
|
-
|
|
281
|
-
Rendered once and cached forever. Best for content that rarely changes.
|
|
282
|
-
|
|
283
|
-
```html
|
|
284
|
-
<script server>
|
|
285
|
-
const metadata = { title: "Docs", static: true };
|
|
286
|
-
|
|
287
|
-
async function getData() {
|
|
288
|
-
return { content: await fetchDocs() };
|
|
289
|
-
}
|
|
290
|
-
</script>
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### ISR — Incremental Static Regeneration
|
|
294
|
-
|
|
295
|
-
Cached but automatically regenerated after N seconds. Best of speed and freshness.
|
|
296
|
-
|
|
297
|
-
```html
|
|
298
|
-
<script server>
|
|
299
|
-
const metadata = {
|
|
300
|
-
title: "Weather",
|
|
301
|
-
revalidate: 60, // regenerate every 60 s
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
async function getData({ req }) {
|
|
305
|
-
const { city } = req.params;
|
|
306
|
-
const weather = await fetchWeather(city);
|
|
307
|
-
return { city, weather };
|
|
308
|
-
}
|
|
309
|
-
</script>
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
**`revalidate` values:**
|
|
313
|
-
|
|
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 |
|
|
321
|
-
|
|
322
|
-
## 📐 Layouts
|
|
323
|
-
|
|
324
|
-
### Root layout
|
|
325
|
-
|
|
326
|
-
`pages/layout.vex` wraps every page:
|
|
327
|
-
|
|
328
|
-
```html
|
|
329
|
-
<script server>
|
|
330
|
-
const props = xprops({ children: { default: "" } });
|
|
331
|
-
</script>
|
|
332
|
-
|
|
333
|
-
<template>
|
|
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>
|
|
342
|
-
</template>
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### Nested layouts
|
|
346
|
-
|
|
347
|
-
Add a `layout.vex` inside any subdirectory:
|
|
348
|
-
|
|
349
|
-
```
|
|
350
|
-
pages/
|
|
351
|
-
layout.vex ← wraps everything
|
|
352
|
-
docs/
|
|
353
|
-
layout.vex ← wraps /docs/* only
|
|
354
|
-
page.vex
|
|
355
|
-
getting-started/page.vex
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
## ⏳ Suspense (Streaming)
|
|
50
|
+
## Documentation
|
|
359
51
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
<template>
|
|
369
|
-
<Suspense :fallback="<SkeletonCard />">
|
|
370
|
-
<SlowCard :userId="1" />
|
|
371
|
-
</Suspense>
|
|
372
|
-
</template>
|
|
373
|
-
```
|
|
374
|
-
|
|
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.
|
|
376
|
-
|
|
377
|
-
## 🔄 Reactive System
|
|
378
|
-
|
|
379
|
-
Mirrors Vue 3's Composition API. Import from `vex/reactive` in `<script client>` blocks.
|
|
380
|
-
|
|
381
|
-
### `reactive(value)`
|
|
382
|
-
|
|
383
|
-
```js
|
|
384
|
-
import { reactive } from "vex/reactive";
|
|
385
|
-
|
|
386
|
-
// Primitives → access via .value
|
|
387
|
-
const count = reactive(0);
|
|
388
|
-
count.value++;
|
|
389
|
-
|
|
390
|
-
// Objects → direct property access
|
|
391
|
-
const state = reactive({ x: 1, name: "Alice" });
|
|
392
|
-
state.x++;
|
|
393
|
-
state.name = "Bob";
|
|
394
|
-
```
|
|
395
|
-
|
|
396
|
-
### `computed(getter)`
|
|
397
|
-
|
|
398
|
-
```js
|
|
399
|
-
import { reactive, computed } from "vex/reactive";
|
|
400
|
-
|
|
401
|
-
const price = reactive(100);
|
|
402
|
-
const qty = reactive(2);
|
|
403
|
-
const total = computed(() => price.value * qty.value);
|
|
404
|
-
|
|
405
|
-
console.log(total.value); // 200
|
|
406
|
-
price.value = 150;
|
|
407
|
-
console.log(total.value); // 300
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
### `effect(fn)`
|
|
411
|
-
|
|
412
|
-
Runs immediately and re-runs whenever its reactive dependencies change.
|
|
413
|
-
|
|
414
|
-
```js
|
|
415
|
-
import { reactive, effect } from "vex/reactive";
|
|
416
|
-
|
|
417
|
-
const count = reactive(0);
|
|
418
|
-
const stop = effect(() => document.title = `Count: ${count.value}`);
|
|
419
|
-
|
|
420
|
-
count.value++; // effect re-runs
|
|
421
|
-
stop(); // cleanup
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
### `watch(source, callback)`
|
|
425
|
-
|
|
426
|
-
Runs only when the source changes (not on creation).
|
|
427
|
-
|
|
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
|
-
});
|
|
435
|
-
```
|
|
436
|
-
|
|
437
|
-
### Reactivity summary
|
|
438
|
-
|
|
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 | — |
|
|
445
|
-
|
|
446
|
-
## 📝 Template Syntax
|
|
447
|
-
|
|
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) |
|
|
456
|
-
|
|
457
|
-
```html
|
|
458
|
-
<template>
|
|
459
|
-
<h1>Hello, {{name}}</h1>
|
|
460
|
-
|
|
461
|
-
<ul>
|
|
462
|
-
<li x-for="item in items">{{item}}</li>
|
|
463
|
-
</ul>
|
|
464
|
-
|
|
465
|
-
<div x-if="isVisible">Visible</div>
|
|
466
|
-
|
|
467
|
-
<button :disabled="count.value <= 0" @click="count.value--">-</button>
|
|
468
|
-
</template>
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
> Keep logic in `getData` rather than inline expressions. Ternaries and filters are not supported in templates.
|
|
472
|
-
|
|
473
|
-
## 🛣️ Routing
|
|
474
|
-
|
|
475
|
-
### File-based routes
|
|
476
|
-
|
|
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 |
|
|
484
|
-
|
|
485
|
-
### Dynamic routes
|
|
486
|
-
|
|
487
|
-
```html
|
|
488
|
-
<!-- pages/users/[id]/page.vex -->
|
|
489
|
-
<script server>
|
|
490
|
-
async function getData({ req }) {
|
|
491
|
-
const { id } = req.params;
|
|
492
|
-
return { user: await fetchUser(id) };
|
|
493
|
-
}
|
|
494
|
-
</script>
|
|
495
|
-
|
|
496
|
-
<template>
|
|
497
|
-
<h1>{{user.name}}</h1>
|
|
498
|
-
</template>
|
|
499
|
-
```
|
|
500
|
-
|
|
501
|
-
### Pre-generate dynamic pages (SSG)
|
|
502
|
-
|
|
503
|
-
```js
|
|
504
|
-
// inside <script server>
|
|
505
|
-
export async function getStaticPaths() {
|
|
506
|
-
return [
|
|
507
|
-
{ params: { id: "1" } },
|
|
508
|
-
{ params: { id: "2" } },
|
|
509
|
-
];
|
|
510
|
-
}
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
### Client-side navigation
|
|
514
|
-
|
|
515
|
-
```js
|
|
516
|
-
window.app.navigate("/about");
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
### Route & query params (client)
|
|
520
|
-
|
|
521
|
-
```js
|
|
522
|
-
import { useRouteParams } from "vex/navigation";
|
|
523
|
-
import { useQueryParams } from "vex/navigation";
|
|
524
|
-
|
|
525
|
-
const { id } = useRouteParams(); // reactive, updates on navigation
|
|
526
|
-
const { search } = useQueryParams();
|
|
527
|
-
```
|
|
528
|
-
|
|
529
|
-
## ⚡ Prefetching
|
|
530
|
-
|
|
531
|
-
Add `data-prefetch` to any `<a>` tag to prefetch the page when the link enters the viewport:
|
|
532
|
-
|
|
533
|
-
```html
|
|
534
|
-
<a href="/about" data-prefetch>About</a>
|
|
535
|
-
```
|
|
536
|
-
|
|
537
|
-
The page component is loaded in the background; navigation to it is instant.
|
|
538
|
-
|
|
539
|
-
## 🎨 Styling
|
|
540
|
-
|
|
541
|
-
The framework uses **Tailwind CSS v4**. The dev script watches `src/input.css` and outputs to `public/styles.css`.
|
|
542
|
-
|
|
543
|
-
```css
|
|
544
|
-
/* src/input.css */
|
|
545
|
-
@import "tailwindcss";
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
Reference the stylesheet in `root.html`:
|
|
549
|
-
|
|
550
|
-
```html
|
|
551
|
-
<link rel="stylesheet" href="/styles.css">
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
## 🔧 Framework API
|
|
555
|
-
|
|
556
|
-
### Import conventions
|
|
557
|
-
|
|
558
|
-
| Pattern | Example | Behaviour |
|
|
559
|
-
|---------|---------|-----------|
|
|
560
|
-
| `vex/*` | `import { reactive } from "vex/reactive"` | Framework singleton — shared instance across all components |
|
|
561
|
-
| `@/*` | `import store from "@/utils/store.js"` | Project alias for your source root — also a singleton |
|
|
562
|
-
| `./` / `../` | `import { fn } from "./helpers.js"` | Relative user file — also a singleton |
|
|
563
|
-
| npm bare specifier | `import { format } from "date-fns"` | Bundled inline by esbuild |
|
|
564
|
-
|
|
565
|
-
All user JS files (`@/` and relative) are pre-bundled at startup: npm packages are inlined, while `vex/*`, `@/*`, and relative imports stay external. The browser's ES module cache guarantees every import of the same file returns the same instance — enabling shared reactive state across components without a dedicated store library.
|
|
566
|
-
|
|
567
|
-
### Client script imports
|
|
568
|
-
|
|
569
|
-
| Import | Description |
|
|
570
|
-
|--------|-------------|
|
|
571
|
-
| `vex/reactive` | Reactivity engine (`reactive`, `computed`, `effect`, `watch`) |
|
|
572
|
-
| `vex/navigation` | Router utilities (`useRouteParams`, `useQueryParams`) |
|
|
573
|
-
|
|
574
|
-
### Server script hooks
|
|
575
|
-
|
|
576
|
-
| Export | Description |
|
|
577
|
-
|--------|-------------|
|
|
578
|
-
| `async getData({ req, props })` | Fetches data; return value is merged into template scope |
|
|
579
|
-
| `metadata` / `async getMetadata({ req, props })` | Page-level config (`title`, `description`, `static`, `revalidate`) |
|
|
580
|
-
| `async getStaticPaths()` | Returns `[{ params }]` for pre-rendering dynamic routes |
|
|
581
|
-
|
|
582
|
-
## 📦 Available Scripts
|
|
583
|
-
|
|
584
|
-
```bash
|
|
585
|
-
vex dev # Start dev server with HMR (--watch)
|
|
586
|
-
vex build # Pre-render pages, generate routes, bundle client JS
|
|
587
|
-
vex start # Production server (requires a prior build)
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
> `vex start` requires `vex build` to have been run first.
|
|
591
|
-
|
|
592
|
-
## 🏗️ Rendering Flow
|
|
593
|
-
|
|
594
|
-
### SSR (Server-Side Rendering)
|
|
595
|
-
|
|
596
|
-
```mermaid
|
|
597
|
-
sequenceDiagram
|
|
598
|
-
participant Client
|
|
599
|
-
participant Server
|
|
600
|
-
participant Router
|
|
601
|
-
participant ComponentProcessor
|
|
602
|
-
participant Streaming
|
|
603
|
-
participant Cache
|
|
604
|
-
|
|
605
|
-
Client->>Server: GET /page
|
|
606
|
-
Server->>Router: handlePageRequest(req, res, route)
|
|
607
|
-
Router->>Router: Check ISR cache
|
|
608
|
-
|
|
609
|
-
alt Cache valid
|
|
610
|
-
Router->>Cache: getCachedHtml()
|
|
611
|
-
Cache-->>Router: html
|
|
612
|
-
Router->>Client: Send cached HTML
|
|
613
|
-
else No cache / stale
|
|
614
|
-
Router->>ComponentProcessor: renderPageWithLayout()
|
|
615
|
-
ComponentProcessor->>ComponentProcessor: processHtmlFile → getData → compileTemplate
|
|
616
|
-
ComponentProcessor->>Streaming: renderComponents (Suspense boundaries)
|
|
617
|
-
Streaming-->>ComponentProcessor: { html, suspenseComponents }
|
|
618
|
-
ComponentProcessor-->>Router: { html, suspenseComponents }
|
|
619
|
-
|
|
620
|
-
alt No Suspense
|
|
621
|
-
Router->>Client: Send full HTML
|
|
622
|
-
Router->>Cache: Save (if ISR)
|
|
623
|
-
else Has Suspense
|
|
624
|
-
Router->>Client: Stream initial HTML (skeleton)
|
|
625
|
-
loop Each suspense component
|
|
626
|
-
Router->>Streaming: renderSuspenseComponent()
|
|
627
|
-
Streaming-->>Router: Resolved HTML
|
|
628
|
-
Router->>Client: Stream replacement chunk
|
|
629
|
-
end
|
|
630
|
-
Router->>Client: End stream
|
|
631
|
-
Router->>Cache: Save complete HTML (if ISR)
|
|
632
|
-
end
|
|
633
|
-
end
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
### ISR (Incremental Static Regeneration)
|
|
637
|
-
|
|
638
|
-
```mermaid
|
|
639
|
-
sequenceDiagram
|
|
640
|
-
participant Client
|
|
641
|
-
participant Router
|
|
642
|
-
participant Cache
|
|
643
|
-
participant FileSystem
|
|
644
|
-
|
|
645
|
-
Client->>Router: GET /page
|
|
646
|
-
Router->>Cache: getCachedHtml(url, revalidateSeconds)
|
|
647
|
-
Cache->>FileSystem: Read HTML + meta
|
|
648
|
-
|
|
649
|
-
alt Cache exists and valid
|
|
650
|
-
Cache-->>Router: { html, isStale: false }
|
|
651
|
-
Router->>Client: Instant cached response
|
|
652
|
-
else Cache stale or missing
|
|
653
|
-
Router->>Router: renderPageWithLayout()
|
|
654
|
-
Router->>Client: Fresh response
|
|
655
|
-
Router->>FileSystem: saveComponentHtmlDisk()
|
|
656
|
-
end
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
### Server Startup
|
|
660
|
-
|
|
661
|
-
```mermaid
|
|
662
|
-
sequenceDiagram
|
|
663
|
-
participant CLI
|
|
664
|
-
participant index.js
|
|
665
|
-
participant ComponentProcessor
|
|
666
|
-
|
|
667
|
-
CLI->>index.js: node .../server/index.js
|
|
668
|
-
|
|
669
|
-
alt NODE_ENV=production
|
|
670
|
-
index.js->>index.js: import .vexjs/_routes.js
|
|
671
|
-
note right of index.js: Fails if pnpm build not run
|
|
672
|
-
else development
|
|
673
|
-
index.js->>ComponentProcessor: build()
|
|
674
|
-
ComponentProcessor-->>index.js: serverRoutes
|
|
675
|
-
end
|
|
676
|
-
|
|
677
|
-
index.js->>index.js: registerSSRRoutes(app, serverRoutes)
|
|
678
|
-
index.js->>CLI: Listening on :3001
|
|
679
|
-
```
|
|
52
|
+
- [Routing & Project Structure](docs/routing.md)
|
|
53
|
+
- [Components & Layouts](docs/components.md)
|
|
54
|
+
- [Rendering Strategies](docs/rendering.md)
|
|
55
|
+
- [Reactive System](docs/reactivity.md)
|
|
56
|
+
- [Template Syntax](docs/templates.md)
|
|
57
|
+
- [Configuration & API](docs/configuration.md)
|
|
58
|
+
- [Deploy to Vercel](DEPLOY.md)
|
|
680
59
|
|
|
681
|
-
##
|
|
60
|
+
## Roadmap
|
|
682
61
|
|
|
683
62
|
- [x] File-based routing with dynamic segments
|
|
684
63
|
- [x] SSR / CSR / SSG / ISR rendering strategies
|
|
@@ -697,17 +76,16 @@ sequenceDiagram
|
|
|
697
76
|
- [x] `vex.config.json` — configurable `srcDir` and `watchIgnore`
|
|
698
77
|
- [x] Published to npm as `@cfdez11/vex`
|
|
699
78
|
- [x] VS Code extension with syntax highlighting and go-to-definition
|
|
700
|
-
- [ ] Refactor client component prop pipeline
|
|
701
|
-
- [ ] esbuild minification
|
|
702
|
-
- [ ] esbuild source maps
|
|
703
|
-
- [ ] esbuild browser target
|
|
704
|
-
- [ ] esbuild code splitting
|
|
79
|
+
- [ ] Refactor client component prop pipeline
|
|
80
|
+
- [ ] esbuild minification for production builds
|
|
81
|
+
- [ ] esbuild source maps in dev mode
|
|
82
|
+
- [ ] esbuild browser target config
|
|
83
|
+
- [ ] esbuild code splitting for shared dependencies
|
|
705
84
|
- [ ] Devtools
|
|
706
|
-
- [ ]
|
|
707
|
-
- [ ]
|
|
708
|
-
- [ ]
|
|
709
|
-
- [ ]
|
|
710
|
-
- [ ] Create docs page
|
|
85
|
+
- [ ] TypeScript support (framework + user code)
|
|
86
|
+
- [ ] Improved VS Code extension
|
|
87
|
+
- [ ] Theme syntax
|
|
88
|
+
- [ ] Docs page
|
|
711
89
|
- [ ] Authentication middleware
|
|
712
90
|
- [ ] CDN cache integration
|
|
713
91
|
- [ ] Fix Suspense marker replacement with multi-root templates
|
package/dist/server/prebuild.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"dotenv/config";import{build}from"./utils/component-processor.js";import{initializeDirectories}from"./utils/files.js";console.log("\u{1F528} Starting prebuild..."),console.log("\u{1F4C1} Creating directories..."),await initializeDirectories(),console.log("\u2699\uFE0F Generating components and routes..."),await build(),console.log("\u2705 Prebuild complete!");
|
|
1
|
+
import"dotenv/config";import{build}from"./utils/component-processor.js";import{initializeDirectories}from"./utils/files.js";console.log("\u{1F528} Starting prebuild..."),console.log("\u{1F4C1} Creating directories..."),await initializeDirectories(),console.log("\u2699\uFE0F Generating components and routes..."),await build(),console.log("\u2705 Prebuild complete!"),process.exit(0);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from"fs/promises";import{watch,existsSync,statSync,readFileSync}from"fs";import path from"path";import crypto from"crypto";import{fileURLToPath,pathToFileURL}from"url";import esbuild from"esbuild";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),FRAMEWORK_DIR=path.resolve(__dirname,"..",".."),PROJECT_ROOT=process.cwd(),ROOT_DIR=PROJECT_ROOT;let _vexConfig={};try{_vexConfig=JSON.parse(readFileSync(path.join(PROJECT_ROOT,"vex.config.json"),"utf-8"))}catch{}const SRC_DIR=path.resolve(PROJECT_ROOT,_vexConfig.srcDir||"."),WATCH_IGNORE=new Set(["dist","build","out",".output",".vexjs","public","node_modules",".git",".svn","coverage",".nyc_output",".next",".nuxt",".svelte-kit",".astro","tmp","temp",".cache",".claude",...(_vexConfig.watchIgnore||[]).filter(p=>!/[\/\*\.]/.test(p))]),WATCH_IGNORE_FILES=
|
|
1
|
+
import fs from"fs/promises";import{watch,existsSync,statSync,readFileSync}from"fs";import path from"path";import crypto from"crypto";import{fileURLToPath,pathToFileURL}from"url";import esbuild from"esbuild";const __filename=fileURLToPath(import.meta.url),__dirname=path.dirname(__filename),FRAMEWORK_DIR=path.resolve(__dirname,"..",".."),PROJECT_ROOT=process.cwd(),ROOT_DIR=PROJECT_ROOT;let _vexConfig={};try{_vexConfig=JSON.parse(readFileSync(path.join(PROJECT_ROOT,"vex.config.json"),"utf-8"))}catch{}const SRC_DIR=path.resolve(PROJECT_ROOT,_vexConfig.srcDir||"."),WATCH_IGNORE=new Set(["dist","build","out",".output",".vexjs","public","node_modules",".git",".svn","coverage",".nyc_output",".next",".nuxt",".svelte-kit",".astro","tmp","temp",".cache",".claude",...(_vexConfig.watchIgnore||[]).filter(p=>!/[\/\*\.]/.test(p))]),WATCH_IGNORE_FILES=(_vexConfig.watchIgnore||[]).filter(p=>/[\/\*\.]/.test(p)),PAGES_DIR=path.resolve(SRC_DIR,"pages"),SERVER_APP_DIR=path.join(FRAMEWORK_DIR,"server"),CLIENT_DIR=path.join(FRAMEWORK_DIR,"client"),CLIENT_SERVICES_DIR=path.join(CLIENT_DIR,"services"),GENERATED_DIR=path.join(PROJECT_ROOT,".vexjs"),CACHE_DIR=path.join(GENERATED_DIR,"_cache"),CLIENT_COMPONENTS_DIR=path.join(GENERATED_DIR,"_components"),USER_GENERATED_DIR=path.join(GENERATED_DIR,"user"),ROOT_HTML_USER=path.join(PROJECT_ROOT,"root.html"),ROOT_HTML_DEFAULT=path.join(FRAMEWORK_DIR,"server","root.html"),ROOT_HTML_DIR=ROOT_HTML_USER;async function minifyServicesDir(src,dest){const jsFiles=[],otherFiles=[],collect=async(srcDir,destDir)=>{const entries=await fs.readdir(srcDir,{withFileTypes:!0});await Promise.all(entries.map(async entry=>{const srcPath=path.join(srcDir,entry.name),destPath=path.join(destDir,entry.name);entry.isDirectory()?(await fs.mkdir(destPath,{recursive:!0}),await collect(srcPath,destPath)):entry.name.endsWith(".js")?jsFiles.push({in:srcPath,out:destPath}):otherFiles.push({src:srcPath,dest:destPath})}))};await collect(src,dest),await Promise.all([esbuild.build({entryPoints:jsFiles.map(f=>f.in),bundle:!1,format:"esm",platform:"browser",minify:!0,legalComments:"none",outdir:dest,outbase:src,logLevel:"silent"}),...otherFiles.map(f=>fs.copyFile(f.src,f.dest))])}async function initializeDirectories(){try{const servicesDir=path.join(GENERATED_DIR,"services");return await Promise.all([fs.mkdir(GENERATED_DIR,{recursive:!0}),fs.mkdir(CACHE_DIR,{recursive:!0}),fs.mkdir(CLIENT_COMPONENTS_DIR,{recursive:!0}),fs.mkdir(USER_GENERATED_DIR,{recursive:!0}),fs.mkdir(servicesDir,{recursive:!0})]),process.env.NODE_ENV==="production"?await minifyServicesDir(CLIENT_SERVICES_DIR,servicesDir):await fs.cp(CLIENT_SERVICES_DIR,servicesDir,{recursive:!0}),!0}catch(err){console.error("Failed to create cache directory:",err)}}function adjustClientModulePath(modulePath,importStatement,componentFilePath=null){if(modulePath.startsWith("/_vexjs/"))return{path:modulePath,importStatement};const isRelative=(modulePath.startsWith("./")||modulePath.startsWith("../"))&&componentFilePath,isAtAlias=modulePath.startsWith("@/")||modulePath==="@";if(isRelative||isAtAlias){let resolvedPath;if(isAtAlias)resolvedPath=path.resolve(SRC_DIR,modulePath.replace(/^@\//,"").replace(/^@$/,""));else{const componentDir=path.dirname(componentFilePath);resolvedPath=path.resolve(componentDir,modulePath)}path.extname(resolvedPath)||(existsSync(resolvedPath+".js")?resolvedPath+=".js":existsSync(path.join(resolvedPath,"index.js"))?resolvedPath=path.join(resolvedPath,"index.js"):resolvedPath+=".js");const adjustedPath2=`/_vexjs/user/${path.relative(SRC_DIR,resolvedPath).replace(/\\/g,"/")}`,adjustedImportStatement2=importStatement.replace(modulePath,adjustedPath2);return{path:adjustedPath2,importStatement:adjustedImportStatement2}}let relative=modulePath.replace(/^vex\//,""),adjustedPath=`/_vexjs/services/${relative}`;const fsPath=path.join(CLIENT_SERVICES_DIR,relative);existsSync(fsPath)&&statSync(fsPath).isDirectory()?adjustedPath+="/index.js":path.extname(adjustedPath)||(adjustedPath+=".js");const adjustedImportStatement=importStatement.replace(modulePath,adjustedPath);return{path:adjustedPath,importStatement:adjustedImportStatement}}function getRelativePath(from,to){return path.relative(from,to)}function getDirectoryName(filePath){return path.dirname(filePath)}const layoutPathsCache=new Map;process.env.NODE_ENV!=="production"&&watch(PAGES_DIR,{recursive:!0},(_,filename)=>{(filename==="layout.vex"||filename?.endsWith(`${path.sep}layout.vex`))&&layoutPathsCache.clear()});async function _getLayoutPaths(pagePath){const layouts=[],relativePath=getRelativePath(PAGES_DIR,pagePath),pathSegments=getDirectoryName(relativePath).split(path.sep),baseLayout=path.join(PAGES_DIR,"layout.vex");await fileExists(baseLayout)&&layouts.push(baseLayout);let currentPath=PAGES_DIR;for(const segment of pathSegments){if(segment==="."||segment==="..")continue;currentPath=path.join(currentPath,segment);const layoutPath=path.join(currentPath,"layout.vex");await fileExists(layoutPath)&&layouts.push(layoutPath)}return layouts}async function getLayoutPaths(pagePath){if(layoutPathsCache.has(pagePath))return layoutPathsCache.get(pagePath);const result=await _getLayoutPaths(pagePath);return layoutPathsCache.set(pagePath,result),result}function formatFileContent(content){return content.trim()}async function writeFile(filePath,content){const formattedContent=formatFileContent(content);return fs.writeFile(filePath,formattedContent,"utf-8")}function readFile(filePath){return fs.readFile(filePath,"utf-8")}async function fileExists(filePath){try{return await fs.access(filePath),!0}catch{return!1}}function getAutogeneratedComponentName(componentPath){return`_${componentPath.replace(ROOT_DIR+path.sep,"").split(path.sep).filter(Boolean).join("_").replaceAll(".vex","").replaceAll(path.sep,"_").replaceAll("-","_").replaceAll(":","")}`}function generateComponentId(componentPath,options={}){const{length=8,prefix=!0}=options,relativePath=componentPath.replace(ROOT_DIR+path.sep,""),hash=crypto.createHash("sha256").update(relativePath).digest("hex").slice(0,length),baseName=getAutogeneratedComponentName(componentPath).replace(/^_/,"");return prefix?`_${baseName}_${hash}`:hash}const getPagePath=pageName=>path.resolve(PAGES_DIR,pageName,"page.vex"),getRootTemplate=async()=>{try{return await fs.access(ROOT_HTML_USER),await fs.readFile(ROOT_HTML_USER,"utf-8")}catch{return await fs.readFile(ROOT_HTML_DEFAULT,"utf-8")}};async function readDirectoryRecursive(dir){const entries=await fs.readdir(dir,{withFileTypes:!0}),files=[];for(const entry of entries){const fullpath=path.join(dir,entry.name);entry.isDirectory()?files.push(...await readDirectoryRecursive(fullpath)):files.push({path:fullpath.replace(ROOT_DIR,""),fullpath,name:entry.name})}return files}const getComponentNameFromPath=(fullFilepath,fileName)=>{const filePath=fullFilepath.replace(ROOT_DIR+path.sep,"");if(filePath.startsWith(path.join("pages",path.sep))){const segments=filePath.split(path.sep);return segments.length===2?segments[0].replace(".vex",""):segments[segments.length-2].replace(".vex","")}return fileName.replace(".vex","")};async function getComponentHtmlDisk({componentPath}){const filePath=path.join(CACHE_DIR,generateComponentId(componentPath)+".html"),metaPath=filePath+".meta.json",[existsHtml,existsMeta]=await Promise.all([fileExists(filePath),fileExists(metaPath)]);if(!existsMeta||!existsHtml)return{html:null,meta:null};const[html,meta]=await Promise.all([fs.readFile(filePath,"utf-8"),fs.readFile(metaPath,"utf-8")]).then(([htmlContent,metaContent])=>[htmlContent,JSON.parse(metaContent)]);return{html,meta}}async function saveComponentHtmlDisk({componentPath,html}){const filePath=path.join(CACHE_DIR,generateComponentId(componentPath)+".html"),metaPath=filePath+".meta.json",meta={generatedAt:Date.now(),isStale:!1,path:componentPath};await Promise.all([writeFile(filePath,html,"utf-8"),writeFile(metaPath,JSON.stringify(meta),"utf-8")])}async function markComponentHtmlStale({componentPath}){const metaPath=path.join(CACHE_DIR,generateComponentId(componentPath)+".html")+".meta.json";if(!await fileExists(metaPath))return;const meta=JSON.parse(await fs.readFile(metaPath,"utf-8"));meta.isStale=!0,await writeFile(metaPath,JSON.stringify(meta),"utf-8")}async function saveServerRoutesFile(serverRoutes){await writeFile(path.join(GENERATED_DIR,"_routes.js"),`// Auto-generated by prebuild \u2014 do not edit manually.
|
|
2
2
|
export const routes = ${JSON.stringify(serverRoutes,null,2)};
|
|
3
3
|
`)}async function saveClientRoutesFile(clientRoutes){const clientFileCode=`
|
|
4
4
|
import { loadRouteComponent } from './cache.js';
|
package/package.json
CHANGED