@cfdez11/vex 0.1.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 +1383 -0
- package/client/app.webmanifest +14 -0
- package/client/favicon.ico +0 -0
- package/client/services/cache.js +55 -0
- package/client/services/hmr-client.js +22 -0
- package/client/services/html.js +377 -0
- package/client/services/hydrate-client-components.js +97 -0
- package/client/services/hydrate.js +25 -0
- package/client/services/index.js +9 -0
- package/client/services/navigation/create-layouts.js +172 -0
- package/client/services/navigation/create-navigation.js +103 -0
- package/client/services/navigation/index.js +8 -0
- package/client/services/navigation/link-interceptor.js +39 -0
- package/client/services/navigation/metadata.js +23 -0
- package/client/services/navigation/navigate.js +64 -0
- package/client/services/navigation/prefetch.js +43 -0
- package/client/services/navigation/render-page.js +45 -0
- package/client/services/navigation/render-ssr.js +157 -0
- package/client/services/navigation/router.js +48 -0
- package/client/services/navigation/use-query-params.js +225 -0
- package/client/services/navigation/use-route-params.js +76 -0
- package/client/services/reactive.js +231 -0
- package/package.json +24 -0
- package/server/index.js +115 -0
- package/server/prebuild.js +12 -0
- package/server/root.html +15 -0
- package/server/utils/cache.js +89 -0
- package/server/utils/component-processor.js +1526 -0
- package/server/utils/data-cache.js +62 -0
- package/server/utils/delay.js +1 -0
- package/server/utils/files.js +723 -0
- package/server/utils/hmr.js +21 -0
- package/server/utils/router.js +373 -0
- package/server/utils/streaming.js +315 -0
- package/server/utils/template.js +263 -0
package/README.md
ADDED
|
@@ -0,0 +1,1383 @@
|
|
|
1
|
+
# Vanilla JS Framework
|
|
2
|
+
|
|
3
|
+
[](#)
|
|
4
|
+
[](#)
|
|
5
|
+
[](#)
|
|
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
|
|
83
|
+
|
|
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
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## π Quick Start
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Install dependencies
|
|
146
|
+
pnpm install
|
|
147
|
+
|
|
148
|
+
# Start development server
|
|
149
|
+
pnpm dev
|
|
150
|
+
|
|
151
|
+
# Start production server
|
|
152
|
+
pnpm start
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The server will be available at `http://localhost:3000`
|
|
156
|
+
|
|
157
|
+
## π Creating a Page
|
|
158
|
+
|
|
159
|
+
Pages are created in `pages/` with the following structure:
|
|
160
|
+
|
|
161
|
+
```html
|
|
162
|
+
<!-- pages/example/page.html -->
|
|
163
|
+
<script server>
|
|
164
|
+
// Server-side imports (components)
|
|
165
|
+
import UserCard from "components/user-card.html";
|
|
166
|
+
|
|
167
|
+
// Server-side data fetching
|
|
168
|
+
async function getData() {
|
|
169
|
+
return { message: "Hello from the server" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Page metadata
|
|
173
|
+
const metadata = {
|
|
174
|
+
title: "My Page",
|
|
175
|
+
description: "Page description",
|
|
176
|
+
};
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<script client>
|
|
180
|
+
// Client-side component imports
|
|
181
|
+
import Counter from "components/counter.html";
|
|
182
|
+
</script>
|
|
183
|
+
|
|
184
|
+
<template>
|
|
185
|
+
<h1>{{message}}</h1>
|
|
186
|
+
<Counter start="0" />
|
|
187
|
+
<UserCard userId="123" />
|
|
188
|
+
</template>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Routes are auto-generated** from the `pages/` folder structure. No need to manually register routes!
|
|
192
|
+
|
|
193
|
+
## π§© Components
|
|
194
|
+
|
|
195
|
+
Components are defined in `.html` files within the `components/` folder. They can be either client-side or server-side components.
|
|
196
|
+
|
|
197
|
+
### Component Structure
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
<!-- components/counter.html -->
|
|
201
|
+
<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
|
+
}
|
|
220
|
+
|
|
221
|
+
// Computed values
|
|
222
|
+
const stars = computed(() => Array.from({ length: counter.value }, () => "β"));
|
|
223
|
+
</script>
|
|
224
|
+
|
|
225
|
+
<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>
|
|
235
|
+
</div>
|
|
236
|
+
</template>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Server Components
|
|
240
|
+
|
|
241
|
+
Server components are rendered on the backend and support async data fetching:
|
|
242
|
+
|
|
243
|
+
```html
|
|
244
|
+
<!-- components/user-card.html -->
|
|
245
|
+
<script server>
|
|
246
|
+
const props = vprops({
|
|
247
|
+
userId: { required: true },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
async function getData() {
|
|
251
|
+
const user = await fetch(`https://api.example.com/users/${props.userId}`)
|
|
252
|
+
.then(res => res.json());
|
|
253
|
+
return { user };
|
|
254
|
+
}
|
|
255
|
+
</script>
|
|
256
|
+
|
|
257
|
+
<template>
|
|
258
|
+
<div class="user-card">
|
|
259
|
+
<h3>{{user.name}}</h3>
|
|
260
|
+
<p>{{user.email}}</p>
|
|
261
|
+
</div>
|
|
262
|
+
</template>
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Using Components
|
|
266
|
+
|
|
267
|
+
Import and use components in your pages:
|
|
268
|
+
|
|
269
|
+
```html
|
|
270
|
+
<!-- In pages/page.html -->
|
|
271
|
+
<script client>
|
|
272
|
+
import Counter from "components/counter.html";
|
|
273
|
+
</script>
|
|
274
|
+
|
|
275
|
+
<script server>
|
|
276
|
+
import UserCard from "components/user-card.html";
|
|
277
|
+
</script>
|
|
278
|
+
|
|
279
|
+
<template>
|
|
280
|
+
<Counter start="5" />
|
|
281
|
+
<UserCard userId="123" />
|
|
282
|
+
</template>
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## π Rendering Strategies
|
|
286
|
+
|
|
287
|
+
The framework supports multiple rendering strategies to optimize performance and user experience based on your needs.
|
|
288
|
+
|
|
289
|
+
### SSR - Server-Side Rendering
|
|
290
|
+
|
|
291
|
+
**When to use**: Dynamic content that changes frequently, SEO-critical pages, personalized content.
|
|
292
|
+
|
|
293
|
+
Pages are rendered on the server for each request. HTML is generated with fresh data and sent to the client.
|
|
294
|
+
|
|
295
|
+
```html
|
|
296
|
+
<!-- pages/page-ssr/page.html -->
|
|
297
|
+
<script server>
|
|
298
|
+
async function getData() {
|
|
299
|
+
// Fresh data on every request
|
|
300
|
+
const data = await fetch('https://api.example.com/data').then(r => r.json());
|
|
301
|
+
return { data };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const metadata = {
|
|
305
|
+
title: "SSR Page",
|
|
306
|
+
description: "Server-rendered on every request"
|
|
307
|
+
};
|
|
308
|
+
</script>
|
|
309
|
+
|
|
310
|
+
<template>
|
|
311
|
+
<h1>{{data.title}}</h1>
|
|
312
|
+
<p>Generated at: {{new Date().toISOString()}}</p>
|
|
313
|
+
</template>
|
|
314
|
+
```
|
|
315
|
+
|
|
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
|
|
323
|
+
|
|
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.
|
|
327
|
+
|
|
328
|
+
```html
|
|
329
|
+
<!-- pages/page-csr/page.html -->
|
|
330
|
+
<script client>
|
|
331
|
+
import { reactive } from ".app/reactive.js";
|
|
332
|
+
|
|
333
|
+
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();
|
|
342
|
+
</script>
|
|
343
|
+
|
|
344
|
+
<template>
|
|
345
|
+
<div v-if="data">
|
|
346
|
+
<h1>{{data.title}}</h1>
|
|
347
|
+
</div>
|
|
348
|
+
<div v-else>
|
|
349
|
+
Loading...
|
|
350
|
+
</div>
|
|
351
|
+
</template>
|
|
352
|
+
```
|
|
353
|
+
|
|
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
|
|
362
|
+
|
|
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.
|
|
366
|
+
|
|
367
|
+
```html
|
|
368
|
+
<!-- pages/static-with-data/page.html -->
|
|
369
|
+
<script server>
|
|
370
|
+
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 };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const metadata = {
|
|
377
|
+
title: "Static Page",
|
|
378
|
+
description: "Pre-rendered at build time",
|
|
379
|
+
revalidate: 'never' // Never regenerate
|
|
380
|
+
};
|
|
381
|
+
</script>
|
|
382
|
+
|
|
383
|
+
<template>
|
|
384
|
+
<h1>{{data.title}}</h1>
|
|
385
|
+
<p>Built at: {{new Date().toISOString()}}</p>
|
|
386
|
+
</template>
|
|
387
|
+
```
|
|
388
|
+
|
|
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).
|
|
399
|
+
|
|
400
|
+
Pages are statically generated but automatically regenerate after a specified time period.
|
|
401
|
+
|
|
402
|
+
```html
|
|
403
|
+
<!-- pages/page-ssr/[city]/page.html -->
|
|
404
|
+
<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
|
+
const metadata = {
|
|
414
|
+
title: "Weather",
|
|
415
|
+
description: "Weather with ISR",
|
|
416
|
+
revalidate: 10 // Regenerate every 10 seconds
|
|
417
|
+
};
|
|
418
|
+
</script>
|
|
419
|
+
|
|
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>
|
|
425
|
+
```
|
|
426
|
+
|
|
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)
|
|
433
|
+
|
|
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
|
+
```
|
|
442
|
+
|
|
443
|
+
## π Layouts
|
|
444
|
+
|
|
445
|
+
The framework supports nested layouts for consistent page structure.
|
|
446
|
+
|
|
447
|
+
### Root Layout
|
|
448
|
+
|
|
449
|
+
The main layout is defined in `pages/layout.html` and wraps all pages:
|
|
450
|
+
|
|
451
|
+
```html
|
|
452
|
+
<!-- pages/layout.html -->
|
|
453
|
+
<script client>
|
|
454
|
+
const props = vprops({
|
|
455
|
+
children: { default: null },
|
|
456
|
+
});
|
|
457
|
+
</script>
|
|
458
|
+
|
|
459
|
+
<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>© 2026 My App</p>
|
|
475
|
+
</footer>
|
|
476
|
+
</div>
|
|
477
|
+
</template>
|
|
478
|
+
```
|
|
479
|
+
|
|
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>
|
|
491
|
+
|
|
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
|
+
```
|
|
503
|
+
|
|
504
|
+
**Layout Hierarchy:**
|
|
505
|
+
```
|
|
506
|
+
pages/layout.html (root layout)
|
|
507
|
+
ββ> pages/static/layout.html (custom layout for /static/*)
|
|
508
|
+
ββ> pages/static/page.html (page content)
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## β³ Suspense (Streaming)
|
|
512
|
+
|
|
513
|
+
Allows showing a fallback while content loads asynchronously:
|
|
514
|
+
|
|
515
|
+
```html
|
|
516
|
+
<script server>
|
|
517
|
+
import UserCardDelayed from "components/user-card-delayed.html";
|
|
518
|
+
import UserCardSkeleton from "components/user-card-skeleton.html";
|
|
519
|
+
</script>
|
|
520
|
+
|
|
521
|
+
<template>
|
|
522
|
+
<Suspense :fallback="<UserCardSkeleton />">
|
|
523
|
+
<UserCardDelayed userId="123" />
|
|
524
|
+
</Suspense>
|
|
525
|
+
</template>
|
|
526
|
+
```
|
|
527
|
+
|
|
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
|
|
534
|
+
|
|
535
|
+
## π Reactive System
|
|
536
|
+
|
|
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.
|
|
544
|
+
|
|
545
|
+
**Use when:** You need reactive state that automatically updates the UI.
|
|
546
|
+
|
|
547
|
+
```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";
|
|
569
|
+
|
|
570
|
+
const count = reactive(0);
|
|
571
|
+
|
|
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
|
|
582
|
+
```
|
|
583
|
+
|
|
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.
|
|
589
|
+
|
|
590
|
+
```js
|
|
591
|
+
import { reactive, computed } from ".app/reactive.js";
|
|
592
|
+
|
|
593
|
+
const price = reactive(100);
|
|
594
|
+
const quantity = reactive(2);
|
|
595
|
+
|
|
596
|
+
// Computed value automatically updates
|
|
597
|
+
const total = computed(() => price.value * quantity.value);
|
|
598
|
+
|
|
599
|
+
console.log(total.value); // 200
|
|
600
|
+
price.value = 150;
|
|
601
|
+
console.log(total.value); // 300 (automatically recalculated)
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
#### `watch(source, callback, options)`
|
|
605
|
+
|
|
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`).
|
|
609
|
+
|
|
610
|
+
```js
|
|
611
|
+
import { reactive, watch } from ".app/reactive.js";
|
|
612
|
+
|
|
613
|
+
const count = reactive(0);
|
|
614
|
+
|
|
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
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### In Components
|
|
633
|
+
|
|
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>
|
|
664
|
+
|
|
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>
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Reactivity Comparison
|
|
676
|
+
|
|
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 |
|
|
683
|
+
|
|
684
|
+
## π Template Syntax
|
|
685
|
+
|
|
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
|
|
691
|
+
|
|
692
|
+
```html
|
|
693
|
+
<template>
|
|
694
|
+
<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
|
+
|
|
714
|
+
```html
|
|
715
|
+
<template>
|
|
716
|
+
<ul>
|
|
717
|
+
<li v-for="item in items">
|
|
718
|
+
{{item}}
|
|
719
|
+
</li>
|
|
720
|
+
</ul>
|
|
721
|
+
</template>
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
### Event Handlers
|
|
725
|
+
|
|
726
|
+
```html
|
|
727
|
+
<template>
|
|
728
|
+
<button @click="increment">Click me</button>
|
|
729
|
+
<input @input="handleInput" />
|
|
730
|
+
</template>
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Attributes
|
|
734
|
+
|
|
735
|
+
```html
|
|
736
|
+
<template>
|
|
737
|
+
<button :disabled="counter <= 0">Decrement</button>
|
|
738
|
+
<div :class="isActive ? 'active' : ''">Content</div>
|
|
739
|
+
</template>
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
## π£οΈ Routing
|
|
743
|
+
|
|
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
|
+
```
|
|
755
|
+
|
|
756
|
+
### Dynamic Routes
|
|
757
|
+
|
|
758
|
+
Create dynamic routes using `[param]` syntax:
|
|
759
|
+
|
|
760
|
+
```html
|
|
761
|
+
<!-- pages/page-ssr/[city]/page.html -->
|
|
762
|
+
<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 };
|
|
769
|
+
}
|
|
770
|
+
</script>
|
|
771
|
+
|
|
772
|
+
<template>
|
|
773
|
+
<h1>Weather for {{city}}</h1>
|
|
774
|
+
</template>
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Client-Side Navigation
|
|
778
|
+
|
|
779
|
+
```js
|
|
780
|
+
import { navigate } from ".app/navigation.js";
|
|
781
|
+
|
|
782
|
+
// Navigate without page reload
|
|
783
|
+
navigate("/page-ssr");
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### Accessing Route Parameters
|
|
787
|
+
|
|
788
|
+
```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();
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
## β‘ Prefetching
|
|
800
|
+
|
|
801
|
+
The framework automatically prefetches pages to improve navigation performance.
|
|
802
|
+
|
|
803
|
+
### Automatic Prefetching
|
|
804
|
+
|
|
805
|
+
Add the `data-prefetch` attribute to any link to prefetch the page when it enters the viewport:
|
|
806
|
+
|
|
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>
|
|
815
|
+
```
|
|
816
|
+
|
|
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
|
|
822
|
+
|
|
823
|
+
**Benefits:**
|
|
824
|
+
- β‘ Near-instant page transitions
|
|
825
|
+
- π― Smart loading (only when visible)
|
|
826
|
+
- πΎ Automatic caching
|
|
827
|
+
- π Works with SPA navigation
|
|
828
|
+
|
|
829
|
+
**Example in layout:**
|
|
830
|
+
```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>
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**Performance tip:** Use prefetching for frequently accessed pages or important navigation paths.
|
|
845
|
+
|
|
846
|
+
## π¨ Styling
|
|
847
|
+
|
|
848
|
+
The project uses **Tailwind CSS v4** via CDN for simplicity and zero configuration.
|
|
849
|
+
|
|
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>
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
**Compilation:**
|
|
865
|
+
|
|
866
|
+
During development, Tailwind CLI watches your files and compiles styles:
|
|
867
|
+
|
|
868
|
+
```bash
|
|
869
|
+
pnpm dev # Runs Tailwind in watch mode + server
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
The compiled CSS is automatically generated in `.app/client/styles.css`.
|
|
873
|
+
|
|
874
|
+
## π§ Framework API
|
|
875
|
+
|
|
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";
|
|
909
|
+
|
|
910
|
+
// Navigate to a route
|
|
911
|
+
navigate("/page-ssr/madrid");
|
|
912
|
+
|
|
913
|
+
// Access route params
|
|
914
|
+
const { city } = useRouteParams();
|
|
915
|
+
|
|
916
|
+
// Access query params
|
|
917
|
+
const { search } = useQueryParams();
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
## π¦ Available Scripts
|
|
921
|
+
|
|
922
|
+
```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
|
|
927
|
+
```
|
|
928
|
+
|
|
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.
|
|
930
|
+
|
|
931
|
+
## ποΈ Rendering Flow
|
|
932
|
+
|
|
933
|
+
### SSR (Server-Side Rendering)
|
|
934
|
+
|
|
935
|
+
```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
|
+
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
|
|
1097
|
+
end
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
Navigation ->> Navigation: currentNavigationController = null
|
|
1101
|
+
Navigation -->> User: Page rendered and interactive
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1104
|
+
### ISR (Incremental Static Regeneration)
|
|
1105
|
+
|
|
1106
|
+
```mermaid
|
|
1107
|
+
---
|
|
1108
|
+
config:
|
|
1109
|
+
theme: mc
|
|
1110
|
+
---
|
|
1111
|
+
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
|
+
```
|
|
1199
|
+
|
|
1200
|
+
**ISR Flow Summary:**
|
|
1201
|
+
|
|
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
|
|
1209
|
+
|
|
1210
|
+
### Server Startup
|
|
1211
|
+
|
|
1212
|
+
Shows how `index.js` behaves differently in production vs development.
|
|
1213
|
+
|
|
1214
|
+
```mermaid
|
|
1215
|
+
---
|
|
1216
|
+
config:
|
|
1217
|
+
theme: mc
|
|
1218
|
+
---
|
|
1219
|
+
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
|
|
1250
|
+
|
|
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/`.
|
|
1310
|
+
|
|
1311
|
+
### Client Hydration
|
|
1312
|
+
|
|
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
|
|
1351
|
+
```
|
|
1352
|
+
|
|
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
|
+
## πΊοΈ Roadmap
|
|
1356
|
+
|
|
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
|