@lolyjs/core 0.2.0-alpha.2 → 0.2.0-alpha.20
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/LICENCE.md +9 -0
- package/README.md +1074 -761
- package/dist/{bootstrap-BiCQmSkx.d.mts → bootstrap-BfGTMUkj.d.mts} +19 -0
- package/dist/{bootstrap-BiCQmSkx.d.ts → bootstrap-BfGTMUkj.d.ts} +19 -0
- package/dist/cli.cjs +16933 -4416
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +16943 -4416
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +14387 -1372
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +295 -57
- package/dist/index.d.ts +295 -57
- package/dist/index.js +15621 -2597
- package/dist/index.js.map +1 -1
- package/dist/index.types-DMOO-uvF.d.mts +221 -0
- package/dist/index.types-DMOO-uvF.d.ts +221 -0
- package/dist/react/cache.cjs +107 -32
- package/dist/react/cache.cjs.map +1 -1
- package/dist/react/cache.d.mts +27 -21
- package/dist/react/cache.d.ts +27 -21
- package/dist/react/cache.js +107 -32
- package/dist/react/cache.js.map +1 -1
- package/dist/react/components.cjs +10 -8
- package/dist/react/components.cjs.map +1 -1
- package/dist/react/components.js +10 -8
- package/dist/react/components.js.map +1 -1
- package/dist/react/hooks.cjs +208 -26
- package/dist/react/hooks.cjs.map +1 -1
- package/dist/react/hooks.d.mts +75 -15
- package/dist/react/hooks.d.ts +75 -15
- package/dist/react/hooks.js +208 -26
- package/dist/react/hooks.js.map +1 -1
- package/dist/react/sockets.cjs +13 -6
- package/dist/react/sockets.cjs.map +1 -1
- package/dist/react/sockets.js +13 -6
- package/dist/react/sockets.js.map +1 -1
- package/dist/react/themes.cjs +61 -18
- package/dist/react/themes.cjs.map +1 -1
- package/dist/react/themes.js +63 -20
- package/dist/react/themes.js.map +1 -1
- package/dist/runtime.cjs +544 -111
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +540 -107
- package/dist/runtime.js.map +1 -1
- package/package.json +49 -4
package/README.md
CHANGED
|
@@ -1,761 +1,1074 @@
|
|
|
1
|
-
# Loly Framework
|
|
2
|
-
|
|
3
|
-
<div align="center">
|
|
4
|
-
|
|
5
|
-
**A modern, full-stack React framework with native WebSocket support, route-level middlewares, and enterprise-grade features**
|
|
6
|
-
|
|
7
|
-
[](https://www.npmjs.com/package/@lolyjs/core)
|
|
8
|
-
[](https://opensource.org/licenses/ISC)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
await
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
export
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
},
|
|
464
|
-
};
|
|
465
|
-
};
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
```tsx
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
//
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1
|
+
# Loly Framework
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
**A modern, full-stack React framework with native WebSocket support, route-level middlewares, and enterprise-grade features**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@lolyjs/core)
|
|
8
|
+
[](https://opensource.org/licenses/ISC)
|
|
9
|
+

|
|
10
|
+
<br>
|
|
11
|
+
[](https://github.com/MenvielleValen/loly-framework)
|
|
12
|
+
[](https://github.com/MenvielleValen/loly-framework)
|
|
13
|
+
|
|
14
|
+
_Built with React 19, Express, Rspack, Socket.IO, and TypeScript_
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Getting Started
|
|
21
|
+
|
|
22
|
+
Create a new Loly application in seconds:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx create-loly-app mi-app
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This will create a new project with all the necessary files and dependencies. For more information about the CLI, visit the [@lolyjs/cli package](https://www.npmjs.com/package/@lolyjs/cli).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Overview
|
|
33
|
+
|
|
34
|
+
Loly is a full-stack React framework that combines the simplicity of file-based routing with powerful server-side rendering, static site generation, and unique features like native WebSocket support and route-level middlewares.
|
|
35
|
+
|
|
36
|
+
### What Makes Loly Different?
|
|
37
|
+
|
|
38
|
+
- 🔌 **Native WebSocket Support** - Built-in Socket.IO integration with automatic namespace routing
|
|
39
|
+
- 🎯 **Route-Level Middlewares** - Define middlewares directly in your routes for pages and APIs
|
|
40
|
+
- 📁 **Separation of Concerns** - Server logic in `page.server.hook.ts` and `layout.server.hook.ts` separate from React components
|
|
41
|
+
- 🚀 **Hybrid Rendering** - SSR, SSG, and CSR with streaming support
|
|
42
|
+
- 🛡️ **Security First** - Built-in rate limiting, validation, sanitization, and security headers
|
|
43
|
+
- ⚡ **Performance** - Fast bundling with Rspack and optimized code splitting
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
### Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install @lolyjs/core react react-dom
|
|
53
|
+
# or
|
|
54
|
+
pnpm add @lolyjs/core react react-dom
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Create Your First Page
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// app/page.tsx
|
|
61
|
+
export default function Home() {
|
|
62
|
+
return <h1>Hello, Loly!</h1>;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Add Server-Side Data
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
// app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
|
|
70
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
71
|
+
|
|
72
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
73
|
+
const data = await fetchData();
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
props: { data },
|
|
77
|
+
metadata: {
|
|
78
|
+
title: "Home Page",
|
|
79
|
+
description: "Welcome to Loly",
|
|
80
|
+
// See "SEO & Metadata" section below for full metadata options
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// app/page.tsx
|
|
88
|
+
export default function Home({ props }) {
|
|
89
|
+
return <h1>{props.data}</h1>;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Start Development Server
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npx loly dev
|
|
97
|
+
# Server runs on http://localhost:3000
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Key Features
|
|
103
|
+
|
|
104
|
+
### 🔌 Native WebSocket Support (Realtime v1)
|
|
105
|
+
|
|
106
|
+
Loly includes production-ready WebSocket support with automatic namespace routing, authentication, validation, rate limiting, and multi-instance scaling. Define WebSocket events using the new `defineWssRoute()` API:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// app/wss/chat/events.ts
|
|
110
|
+
import { defineWssRoute } from "@lolyjs/core";
|
|
111
|
+
import { z } from "zod";
|
|
112
|
+
|
|
113
|
+
export default defineWssRoute({
|
|
114
|
+
// Authentication hook
|
|
115
|
+
auth: async (ctx) => {
|
|
116
|
+
const token = ctx.req.headers.authorization;
|
|
117
|
+
return await verifyToken(token); // Returns user or null
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// Connection hook
|
|
121
|
+
onConnect: (ctx) => {
|
|
122
|
+
console.log("User connected:", ctx.user?.id);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Event handlers with validation, guards, and rate limiting
|
|
126
|
+
events: {
|
|
127
|
+
message: {
|
|
128
|
+
// Schema validation (Zod/Valibot)
|
|
129
|
+
schema: z.object({
|
|
130
|
+
text: z.string().min(1).max(500),
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
// Guard (permissions check)
|
|
134
|
+
guard: ({ user }) => !!user, // Require authentication
|
|
135
|
+
|
|
136
|
+
// Per-event rate limiting
|
|
137
|
+
rateLimit: {
|
|
138
|
+
eventsPerSecond: 10,
|
|
139
|
+
burst: 20,
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Handler
|
|
143
|
+
handler: (ctx) => {
|
|
144
|
+
ctx.actions.broadcast("message", {
|
|
145
|
+
text: ctx.data.text,
|
|
146
|
+
from: ctx.user?.id,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Client-side:**
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { lolySocket } from "@lolyjs/core/sockets";
|
|
158
|
+
|
|
159
|
+
const socket = lolySocket("/chat");
|
|
160
|
+
|
|
161
|
+
socket.on("message", (data) => {
|
|
162
|
+
console.log("Received:", data);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
socket.emit("message", { text: "Hello!" });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Key Features:**
|
|
169
|
+
|
|
170
|
+
- ✅ **Production-ready**: Auth, validation, rate limiting, logging
|
|
171
|
+
- ✅ **Multi-instance**: Redis adapter for horizontal scaling
|
|
172
|
+
- ✅ **State Store**: Shared state across instances (memory/Redis)
|
|
173
|
+
- ✅ **Presence**: User-to-socket mapping for targeted messaging
|
|
174
|
+
- ✅ **Type-safe**: Full TypeScript support
|
|
175
|
+
- ✅ **Automatic namespace creation** from file structure
|
|
176
|
+
- ✅ **Same routing pattern** as pages and APIs
|
|
177
|
+
- ✅ **Built-in helpers**: `emit`, `broadcast`, `toUser()`, `toRoom()`, `join()`, `leave()`
|
|
178
|
+
- ✅ **No manual configuration required** (works out of the box for localhost)
|
|
179
|
+
|
|
180
|
+
**📖 For complete documentation, see [REALTIME.md](./docs/REALTIME.md)**
|
|
181
|
+
|
|
182
|
+
### 🎯 Route-Level Middlewares
|
|
183
|
+
|
|
184
|
+
Define middlewares directly in your routes for fine-grained control. Middlewares run before `getServerSideProps` (pages) or API handlers and can modify `ctx.locals`, set headers, redirect, etc.
|
|
185
|
+
|
|
186
|
+
**For Pages:**
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
// app/dashboard/page.server.hook.ts (preferred) or app/dashboard/server.hook.ts (legacy)
|
|
190
|
+
import type { RouteMiddleware, ServerLoader } from "@lolyjs/core";
|
|
191
|
+
|
|
192
|
+
export const beforeServerData: RouteMiddleware[] = [
|
|
193
|
+
async (ctx, next) => {
|
|
194
|
+
// Authentication
|
|
195
|
+
const token = ctx.req.headers.authorization;
|
|
196
|
+
if (!token) {
|
|
197
|
+
ctx.res.redirect("/login");
|
|
198
|
+
return; // Don't call next() if redirecting
|
|
199
|
+
}
|
|
200
|
+
ctx.locals.user = await verifyToken(token);
|
|
201
|
+
await next(); // Call next() to continue to next middleware or getServerSideProps
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
206
|
+
const user = ctx.locals.user; // Available from middleware
|
|
207
|
+
return { props: { user } };
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**For API Routes:**
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// app/api/protected/route.ts
|
|
215
|
+
import type { ApiMiddleware, ApiContext } from "@lolyjs/core";
|
|
216
|
+
|
|
217
|
+
// Global middleware for all methods (GET, POST, PUT, etc.)
|
|
218
|
+
export const beforeApi: ApiMiddleware[] = [
|
|
219
|
+
async (ctx, next) => {
|
|
220
|
+
// Authentication
|
|
221
|
+
const user = await getUser(ctx.req);
|
|
222
|
+
if (!user) {
|
|
223
|
+
return ctx.Response({ error: "Unauthorized" }, 401);
|
|
224
|
+
}
|
|
225
|
+
ctx.locals.user = user;
|
|
226
|
+
await next();
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
// Method-specific middleware (only runs before POST)
|
|
231
|
+
export const beforePOST: ApiMiddleware[] = [
|
|
232
|
+
async (ctx, next) => {
|
|
233
|
+
// Validation specific to POST
|
|
234
|
+
await next();
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
|
|
238
|
+
// Method-specific middleware (only runs before GET)
|
|
239
|
+
export const beforeGET: ApiMiddleware[] = [
|
|
240
|
+
async (ctx, next) => {
|
|
241
|
+
// Cache logic specific to GET
|
|
242
|
+
await next();
|
|
243
|
+
},
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
export async function GET(ctx: ApiContext) {
|
|
247
|
+
const user = ctx.locals.user;
|
|
248
|
+
return ctx.Response({ user });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function POST(ctx: ApiContext) {
|
|
252
|
+
const user = ctx.locals.user;
|
|
253
|
+
const data = ctx.req.body;
|
|
254
|
+
return ctx.Response({ created: true }, 201);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Key Benefits:**
|
|
259
|
+
|
|
260
|
+
- Middlewares execute before loaders/handlers
|
|
261
|
+
- Share data via `ctx.locals`
|
|
262
|
+
- Method-specific middlewares for APIs
|
|
263
|
+
- Clean separation of concerns
|
|
264
|
+
|
|
265
|
+
### 📁 File-Based Routing
|
|
266
|
+
|
|
267
|
+
Routes are automatically created from your file structure:
|
|
268
|
+
|
|
269
|
+
| File Path | Route |
|
|
270
|
+
| ----------------------------- | --------------------- |
|
|
271
|
+
| `app/page.tsx` | `/` |
|
|
272
|
+
| `app/about/page.tsx` | `/about` |
|
|
273
|
+
| `app/blog/[slug]/page.tsx` | `/blog/:slug` |
|
|
274
|
+
| `app/post/[...path]/page.tsx` | `/post/*` (catch-all) |
|
|
275
|
+
|
|
276
|
+
**Nested Layouts:**
|
|
277
|
+
|
|
278
|
+
**⚠️ Important**: Layouts should NOT include `<html>` or `<body>` tags. The framework automatically handles the base HTML structure. Layouts should only contain content that goes inside the body.
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
// app/layout.tsx (Root layout)
|
|
282
|
+
export default function RootLayout({ children, appName, navigation }) {
|
|
283
|
+
return (
|
|
284
|
+
<div>
|
|
285
|
+
<nav>{navigation}</nav>
|
|
286
|
+
{children}
|
|
287
|
+
<footer>{appName}</footer>
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```tsx
|
|
294
|
+
// app/layout.server.hook.ts (Root layout server hook - same directory as layout.tsx)
|
|
295
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
296
|
+
|
|
297
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
298
|
+
return {
|
|
299
|
+
props: {
|
|
300
|
+
appName: "My App",
|
|
301
|
+
navigation: ["Home", "About", "Blog"],
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
// app/blog/layout.tsx (Nested layout)
|
|
309
|
+
export default function BlogLayout({ children, sectionTitle }) {
|
|
310
|
+
return (
|
|
311
|
+
<div>
|
|
312
|
+
<h1>{sectionTitle}</h1>
|
|
313
|
+
<aside>Sidebar</aside>
|
|
314
|
+
<main>{children}</main>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
// app/blog/layout.server.hook.ts (Nested layout server hook - same directory as layout.tsx)
|
|
322
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
323
|
+
|
|
324
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
325
|
+
return {
|
|
326
|
+
props: {
|
|
327
|
+
sectionTitle: "Blog Section",
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Layout Server Hooks:**
|
|
334
|
+
|
|
335
|
+
Layouts can have their own server hooks that provide stable data across all pages. Props from layout server hooks are automatically merged with page props:
|
|
336
|
+
|
|
337
|
+
- **Layout props** (from `layout.server.hook.ts`) are stable and available to both the layout and all pages
|
|
338
|
+
- **Page props** (from `page.server.hook.ts`) are specific to each page and override layout props if there's a conflict
|
|
339
|
+
- **Combined props** are available to both layouts and pages
|
|
340
|
+
|
|
341
|
+
**File Convention:**
|
|
342
|
+
- Layout server hooks: `app/layout.server.hook.ts` (same directory as `layout.tsx`)
|
|
343
|
+
- Page server hooks: `app/page.server.hook.ts` (preferred) or `app/server.hook.ts` (legacy, backward compatible)
|
|
344
|
+
|
|
345
|
+
### 🚀 Hybrid Rendering
|
|
346
|
+
|
|
347
|
+
Choose the best rendering strategy for each page:
|
|
348
|
+
|
|
349
|
+
**SSR (Server-Side Rendering):**
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
// app/posts/page.server.hook.ts (preferred) or app/posts/server.hook.ts (legacy)
|
|
353
|
+
export const dynamic = "force-dynamic" as const;
|
|
354
|
+
|
|
355
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
356
|
+
const posts = await fetchFreshPosts();
|
|
357
|
+
return { props: { posts } };
|
|
358
|
+
};
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**SSG (Static Site Generation):**
|
|
362
|
+
|
|
363
|
+
```tsx
|
|
364
|
+
// app/blog/[slug]/page.server.hook.ts (preferred) or app/blog/[slug]/server.hook.ts (legacy)
|
|
365
|
+
export const dynamic = "force-static" as const;
|
|
366
|
+
|
|
367
|
+
export const generateStaticParams: GenerateStaticParams = async () => {
|
|
368
|
+
const posts = await getAllPosts();
|
|
369
|
+
return posts.map((post) => ({ slug: post.slug }));
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
373
|
+
const post = await getPost(ctx.params.slug);
|
|
374
|
+
return { props: { post } };
|
|
375
|
+
};
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**CSR (Client-Side Rendering):**
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
// app/dashboard/page.tsx (No page.server.hook.ts)
|
|
382
|
+
import { useState, useEffect } from "react";
|
|
383
|
+
|
|
384
|
+
export default function Dashboard() {
|
|
385
|
+
const [data, setData] = useState(null);
|
|
386
|
+
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
fetchData().then(setData);
|
|
389
|
+
}, []);
|
|
390
|
+
|
|
391
|
+
return <div>{data}</div>;
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### 🔌 API Routes
|
|
396
|
+
|
|
397
|
+
Create RESTful APIs with flexible middleware support:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
// app/api/posts/route.ts
|
|
401
|
+
import type { ApiContext } from "@lolyjs/core";
|
|
402
|
+
import { validate } from "@lolyjs/core";
|
|
403
|
+
import { z } from "zod";
|
|
404
|
+
|
|
405
|
+
const postSchema = z.object({
|
|
406
|
+
title: z.string().min(1),
|
|
407
|
+
content: z.string().min(1),
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
export async function GET(ctx: ApiContext) {
|
|
411
|
+
const posts = await getPosts();
|
|
412
|
+
return ctx.Response({ posts });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function POST(ctx: ApiContext) {
|
|
416
|
+
const data = validate(postSchema, ctx.req.body);
|
|
417
|
+
const post = await createPost(data);
|
|
418
|
+
return ctx.Response({ post }, 201);
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### 📊 SEO & Metadata
|
|
423
|
+
|
|
424
|
+
Loly provides comprehensive metadata support for SEO and social sharing. Metadata can be defined at both layout and page levels, with intelligent merging:
|
|
425
|
+
|
|
426
|
+
**Layout Metadata (Base/Defaults):**
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
// app/layout.server.hook.ts
|
|
430
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
431
|
+
|
|
432
|
+
export const getServerSideProps: ServerLoader = async () => {
|
|
433
|
+
return {
|
|
434
|
+
props: { /* ... */ },
|
|
435
|
+
metadata: {
|
|
436
|
+
// Site-wide defaults
|
|
437
|
+
description: "My awesome site",
|
|
438
|
+
lang: "en",
|
|
439
|
+
robots: "index, follow",
|
|
440
|
+
themeColor: "#000000",
|
|
441
|
+
|
|
442
|
+
// Open Graph defaults
|
|
443
|
+
openGraph: {
|
|
444
|
+
type: "website",
|
|
445
|
+
siteName: "My Site",
|
|
446
|
+
locale: "en_US",
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
// Twitter Card defaults
|
|
450
|
+
twitter: {
|
|
451
|
+
card: "summary_large_image",
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
// Custom meta tags
|
|
455
|
+
metaTags: [
|
|
456
|
+
{ name: "author", content: "My Name" },
|
|
457
|
+
],
|
|
458
|
+
|
|
459
|
+
// Custom link tags (preconnect, etc.)
|
|
460
|
+
links: [
|
|
461
|
+
{ rel: "preconnect", href: "https://api.example.com" },
|
|
462
|
+
],
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**Page Metadata (Overrides Layout):**
|
|
469
|
+
|
|
470
|
+
```tsx
|
|
471
|
+
// app/page.server.hook.ts
|
|
472
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
473
|
+
|
|
474
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
475
|
+
const post = await getPost(ctx.params.slug);
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
props: { post },
|
|
479
|
+
metadata: {
|
|
480
|
+
// Page-specific (overrides layout)
|
|
481
|
+
title: `${post.title} | My Site`,
|
|
482
|
+
description: post.excerpt,
|
|
483
|
+
canonical: `https://mysite.com/blog/${post.slug}`,
|
|
484
|
+
|
|
485
|
+
// Open Graph (inherits type, siteName from layout)
|
|
486
|
+
openGraph: {
|
|
487
|
+
title: post.title,
|
|
488
|
+
description: post.excerpt,
|
|
489
|
+
url: `https://mysite.com/blog/${post.slug}`,
|
|
490
|
+
image: {
|
|
491
|
+
url: post.imageUrl,
|
|
492
|
+
width: 1200,
|
|
493
|
+
height: 630,
|
|
494
|
+
alt: post.title,
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
// Twitter Card (inherits card type from layout)
|
|
499
|
+
twitter: {
|
|
500
|
+
title: post.title,
|
|
501
|
+
description: post.excerpt,
|
|
502
|
+
image: post.imageUrl,
|
|
503
|
+
imageAlt: post.title,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
};
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**Full Metadata API:**
|
|
511
|
+
|
|
512
|
+
```tsx
|
|
513
|
+
interface PageMetadata {
|
|
514
|
+
// Basic fields
|
|
515
|
+
title?: string;
|
|
516
|
+
description?: string;
|
|
517
|
+
lang?: string;
|
|
518
|
+
canonical?: string;
|
|
519
|
+
robots?: string;
|
|
520
|
+
themeColor?: string;
|
|
521
|
+
viewport?: string;
|
|
522
|
+
|
|
523
|
+
// Open Graph
|
|
524
|
+
openGraph?: {
|
|
525
|
+
title?: string;
|
|
526
|
+
description?: string;
|
|
527
|
+
type?: string;
|
|
528
|
+
url?: string;
|
|
529
|
+
image?: string | {
|
|
530
|
+
url: string;
|
|
531
|
+
width?: number;
|
|
532
|
+
height?: number;
|
|
533
|
+
alt?: string;
|
|
534
|
+
};
|
|
535
|
+
siteName?: string;
|
|
536
|
+
locale?: string;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// Twitter Cards
|
|
540
|
+
twitter?: {
|
|
541
|
+
card?: "summary" | "summary_large_image" | "app" | "player";
|
|
542
|
+
title?: string;
|
|
543
|
+
description?: string;
|
|
544
|
+
image?: string;
|
|
545
|
+
imageAlt?: string;
|
|
546
|
+
site?: string;
|
|
547
|
+
creator?: string;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// Custom meta tags
|
|
551
|
+
metaTags?: Array<{
|
|
552
|
+
name?: string;
|
|
553
|
+
property?: string;
|
|
554
|
+
httpEquiv?: string;
|
|
555
|
+
content: string;
|
|
556
|
+
}>;
|
|
557
|
+
|
|
558
|
+
// Custom link tags
|
|
559
|
+
links?: Array<{
|
|
560
|
+
rel: string;
|
|
561
|
+
href: string;
|
|
562
|
+
as?: string;
|
|
563
|
+
crossorigin?: string;
|
|
564
|
+
type?: string;
|
|
565
|
+
}>;
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Key Features:**
|
|
570
|
+
|
|
571
|
+
- **Layout + Page Merging**: Layout metadata provides defaults, page metadata overrides specific fields
|
|
572
|
+
- **Automatic Updates**: Metadata updates automatically during SPA navigation
|
|
573
|
+
- **SSR & SSG Support**: Works in both server-side rendering and static generation
|
|
574
|
+
- **Type-Safe**: Full TypeScript support with `PageMetadata` type
|
|
575
|
+
|
|
576
|
+
### 🛡️ Built-in Security
|
|
577
|
+
|
|
578
|
+
**Rate Limiting:**
|
|
579
|
+
|
|
580
|
+
```tsx
|
|
581
|
+
// loly.config.ts
|
|
582
|
+
import { ServerConfig } from "@lolyjs/core";
|
|
583
|
+
|
|
584
|
+
export const config = (env: string): ServerConfig => {
|
|
585
|
+
return {
|
|
586
|
+
rateLimit: {
|
|
587
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
588
|
+
max: 1000,
|
|
589
|
+
strictMax: 5,
|
|
590
|
+
strictPatterns: ["/api/auth/**"],
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
};
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
**Validation with Zod:**
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
import { validate, ValidationError } from "@lolyjs/core";
|
|
600
|
+
import { z } from "zod";
|
|
601
|
+
|
|
602
|
+
const schema = z.object({
|
|
603
|
+
email: z.string().email(),
|
|
604
|
+
age: z.number().int().min(0).max(150),
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
try {
|
|
608
|
+
const data = validate(schema, req.body);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (error instanceof ValidationError) {
|
|
611
|
+
return Response({ errors: error.format() }, 400);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
**Automatic Sanitization:**
|
|
617
|
+
|
|
618
|
+
Route parameters and query strings are automatically sanitized to prevent XSS attacks.
|
|
619
|
+
|
|
620
|
+
**Security Headers:**
|
|
621
|
+
|
|
622
|
+
Helmet is configured by default with CSP (Content Security Policy) and nonce support.
|
|
623
|
+
|
|
624
|
+
### 📝 Structured Logging
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
import { getRequestLogger, createModuleLogger } from "@lolyjs/core";
|
|
628
|
+
|
|
629
|
+
// Request logger (automatic request ID)
|
|
630
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
631
|
+
const logger = getRequestLogger(ctx.req);
|
|
632
|
+
logger.info("Processing request", { userId: ctx.locals.user?.id });
|
|
633
|
+
return { props: {} };
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Module logger
|
|
637
|
+
const logger = createModuleLogger("my-module");
|
|
638
|
+
logger.info("Module initialized");
|
|
639
|
+
logger.error("Error occurred", error);
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
644
|
+
## Project Structure
|
|
645
|
+
|
|
646
|
+
```
|
|
647
|
+
your-app/
|
|
648
|
+
├── app/
|
|
649
|
+
│ ├── layout.tsx # Root layout
|
|
650
|
+
│ ├── layout.server.hook.ts # Root layout server hook (stable props)
|
|
651
|
+
│ ├── page.tsx # Home page (/)
|
|
652
|
+
│ ├── page.server.hook.ts # Page server hook (preferred) or server.hook.ts (legacy)
|
|
653
|
+
│ ├── _not-found.tsx # Custom 404
|
|
654
|
+
│ ├── _error.tsx # Custom error page
|
|
655
|
+
│ ├── blog/
|
|
656
|
+
│ │ ├── layout.tsx # Blog layout
|
|
657
|
+
│ │ ├── layout.server.hook.ts # Blog layout server hook
|
|
658
|
+
│ │ ├── page.tsx # /blog
|
|
659
|
+
│ │ └── [slug]/
|
|
660
|
+
│ │ ├── page.tsx # /blog/:slug
|
|
661
|
+
│ │ └── page.server.hook.ts # Page server hook
|
|
662
|
+
│ ├── api/
|
|
663
|
+
│ │ └── posts/
|
|
664
|
+
│ │ └── route.ts # /api/posts
|
|
665
|
+
│ └── wss/
|
|
666
|
+
│ └── chat/
|
|
667
|
+
│ └── events.ts # WebSocket namespace /chat
|
|
668
|
+
├── components/ # React components
|
|
669
|
+
├── lib/ # Utilities
|
|
670
|
+
├── public/ # Static files
|
|
671
|
+
├── loly.config.ts # Framework configuration
|
|
672
|
+
├── init.server.ts # Server initialization (DB, services, etc.)
|
|
673
|
+
└── package.json
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## API Reference
|
|
679
|
+
|
|
680
|
+
### Server Loader
|
|
681
|
+
|
|
682
|
+
**Page Server Hook:**
|
|
683
|
+
|
|
684
|
+
```tsx
|
|
685
|
+
// app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
|
|
686
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
687
|
+
|
|
688
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
689
|
+
const { req, res, params, pathname, locals } = ctx;
|
|
690
|
+
|
|
691
|
+
// Fetch data
|
|
692
|
+
const data = await fetchData();
|
|
693
|
+
|
|
694
|
+
// Redirect
|
|
695
|
+
return {
|
|
696
|
+
redirect: {
|
|
697
|
+
destination: "/new-path",
|
|
698
|
+
permanent: true,
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// Not found
|
|
703
|
+
return { notFound: true };
|
|
704
|
+
|
|
705
|
+
// Return props
|
|
706
|
+
return {
|
|
707
|
+
props: { data },
|
|
708
|
+
metadata: {
|
|
709
|
+
title: "Page Title",
|
|
710
|
+
description: "Page description",
|
|
711
|
+
// See "SEO & Metadata" section above for full metadata options
|
|
712
|
+
// including Open Graph, Twitter Cards, canonical URLs, etc.
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
};
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
**Layout Server Hook:**
|
|
719
|
+
|
|
720
|
+
```tsx
|
|
721
|
+
// app/layout.server.hook.ts (same directory as layout.tsx)
|
|
722
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
723
|
+
|
|
724
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
725
|
+
// Fetch stable data that persists across all pages
|
|
726
|
+
const user = await getCurrentUser();
|
|
727
|
+
const navigation = await getNavigation();
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
props: {
|
|
731
|
+
user, // Available to layout and all pages
|
|
732
|
+
navigation, // Available to layout and all pages
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
};
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
**Props Merging:**
|
|
739
|
+
|
|
740
|
+
- Layout props (from `layout.server.hook.ts`) are merged first
|
|
741
|
+
- Page props (from `page.server.hook.ts`) are merged second and override layout props
|
|
742
|
+
- Both layouts and pages receive the combined props
|
|
743
|
+
|
|
744
|
+
```tsx
|
|
745
|
+
// app/layout.tsx
|
|
746
|
+
export default function Layout({ user, navigation, children }) {
|
|
747
|
+
// Receives: user, navigation (from layout.server.hook.ts)
|
|
748
|
+
// Also receives: any props from page.server.hook.ts
|
|
749
|
+
return <div>{/* ... */}</div>;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// app/page.tsx
|
|
753
|
+
export default function Page({ user, navigation, posts }) {
|
|
754
|
+
// Receives: user, navigation (from layout.server.hook.ts)
|
|
755
|
+
// Receives: posts (from page.server.hook.ts)
|
|
756
|
+
return <div>{/* ... */}</div>;
|
|
757
|
+
}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
### API Route Handler
|
|
761
|
+
|
|
762
|
+
```tsx
|
|
763
|
+
import type { ApiContext } from "@lolyjs/core";
|
|
764
|
+
|
|
765
|
+
export async function GET(ctx: ApiContext) {
|
|
766
|
+
return ctx.Response({ data: "value" });
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export async function POST(ctx: ApiContext) {
|
|
770
|
+
return ctx.Response({ created: true }, 201);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export async function DELETE(ctx: ApiContext) {
|
|
774
|
+
return ctx.Response({ deleted: true }, 204);
|
|
775
|
+
}
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### WebSocket Event Handler (New API - Realtime v1)
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
import { defineWssRoute } from "@lolyjs/core";
|
|
782
|
+
import { z } from "zod";
|
|
783
|
+
|
|
784
|
+
export default defineWssRoute({
|
|
785
|
+
auth: async (ctx) => {
|
|
786
|
+
// Authenticate user
|
|
787
|
+
return await getUserFromToken(ctx.req.headers.authorization);
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
onConnect: (ctx) => {
|
|
791
|
+
console.log("User connected:", ctx.user?.id);
|
|
792
|
+
},
|
|
793
|
+
|
|
794
|
+
events: {
|
|
795
|
+
"custom-event": {
|
|
796
|
+
schema: z.object({ message: z.string() }),
|
|
797
|
+
guard: ({ user }) => !!user,
|
|
798
|
+
handler: (ctx) => {
|
|
799
|
+
// Emit to all clients
|
|
800
|
+
ctx.actions.emit("response", { message: "Hello" });
|
|
801
|
+
|
|
802
|
+
// Broadcast to all except sender
|
|
803
|
+
ctx.actions.broadcast("notification", ctx.data);
|
|
804
|
+
|
|
805
|
+
// Send to specific user
|
|
806
|
+
ctx.actions.toUser(userId).emit("private", ctx.data);
|
|
807
|
+
|
|
808
|
+
// Send to room
|
|
809
|
+
ctx.actions.toRoom("room-name").emit("room-message", ctx.data);
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
},
|
|
813
|
+
});
|
|
814
|
+
```
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Client Cache
|
|
818
|
+
|
|
819
|
+
```tsx
|
|
820
|
+
import { revalidate } from "@lolyjs/core/client-cache";
|
|
821
|
+
|
|
822
|
+
export default function Page({ props }) {
|
|
823
|
+
const handleRefresh = async () => {
|
|
824
|
+
await revalidate(); // Refresh current page data
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
return <div>{/* Your UI */}</div>;
|
|
828
|
+
}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Components
|
|
832
|
+
|
|
833
|
+
```tsx
|
|
834
|
+
import { Link } from "@lolyjs/core/components";
|
|
835
|
+
|
|
836
|
+
export default function Navigation() {
|
|
837
|
+
return (
|
|
838
|
+
<nav>
|
|
839
|
+
<Link href="/">Home</Link>
|
|
840
|
+
<Link href="/about">About</Link>
|
|
841
|
+
<Link href="/blog/[slug]" params={{ slug: "my-post" }}>
|
|
842
|
+
My Post
|
|
843
|
+
</Link>
|
|
844
|
+
</nav>
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## Configuration
|
|
852
|
+
|
|
853
|
+
### Framework Configuration
|
|
854
|
+
|
|
855
|
+
Create `loly.config.ts` in your project root to configure the framework:
|
|
856
|
+
|
|
857
|
+
```tsx
|
|
858
|
+
import { FrameworkConfig } from "@lolyjs/core";
|
|
859
|
+
|
|
860
|
+
export default {
|
|
861
|
+
directories: {
|
|
862
|
+
app: "app",
|
|
863
|
+
build: ".loly",
|
|
864
|
+
static: "public",
|
|
865
|
+
},
|
|
866
|
+
server: {
|
|
867
|
+
port: 3000,
|
|
868
|
+
host: "localhost",
|
|
869
|
+
},
|
|
870
|
+
routing: {
|
|
871
|
+
trailingSlash: "ignore",
|
|
872
|
+
caseSensitive: false,
|
|
873
|
+
basePath: "",
|
|
874
|
+
},
|
|
875
|
+
rendering: {
|
|
876
|
+
framework: "react",
|
|
877
|
+
streaming: true,
|
|
878
|
+
ssr: true,
|
|
879
|
+
ssg: true,
|
|
880
|
+
},
|
|
881
|
+
} satisfies FrameworkConfig;
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
### Server Configuration
|
|
885
|
+
|
|
886
|
+
Configure server settings (CORS, rate limiting, WebSocket, etc.) in `loly.config.ts` by exporting a `config` function:
|
|
887
|
+
|
|
888
|
+
```tsx
|
|
889
|
+
// loly.config.ts
|
|
890
|
+
import { ServerConfig } from "@lolyjs/core";
|
|
891
|
+
|
|
892
|
+
export const config = (env: string): ServerConfig => {
|
|
893
|
+
const isDev = env === "development";
|
|
894
|
+
|
|
895
|
+
return {
|
|
896
|
+
bodyLimit: "1mb",
|
|
897
|
+
corsOrigin: isDev ? "*" : ["https://yourdomain.com"],
|
|
898
|
+
rateLimit: {
|
|
899
|
+
windowMs: 15 * 60 * 1000,
|
|
900
|
+
max: 1000,
|
|
901
|
+
strictMax: 5,
|
|
902
|
+
strictPatterns: ["/api/auth/**"],
|
|
903
|
+
},
|
|
904
|
+
// Realtime (WebSocket) configuration
|
|
905
|
+
realtime: {
|
|
906
|
+
enabled: true,
|
|
907
|
+
// For production, configure allowed origins
|
|
908
|
+
// For development, localhost is auto-allowed
|
|
909
|
+
allowedOrigins: isDev ? undefined : ["https://yourdomain.com"],
|
|
910
|
+
// Optional: Configure Redis for multi-instance scaling
|
|
911
|
+
// scale: {
|
|
912
|
+
// mode: "cluster",
|
|
913
|
+
// adapter: { url: "redis://localhost:6379" },
|
|
914
|
+
// stateStore: { name: "redis", url: "redis://localhost:6379" },
|
|
915
|
+
// },
|
|
916
|
+
},
|
|
917
|
+
};
|
|
918
|
+
};
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
**Note:** For local development, Realtime works out of the box without any configuration. The framework automatically allows `localhost` connections. Only configure `allowedOrigins` when deploying to production.
|
|
922
|
+
|
|
923
|
+
### Server Initialization
|
|
924
|
+
|
|
925
|
+
Create `init.server.ts` in your project root to initialize services when Express starts (database connections, external services, etc.):
|
|
926
|
+
|
|
927
|
+
```tsx
|
|
928
|
+
// init.server.ts
|
|
929
|
+
import { InitServerData } from "@lolyjs/core";
|
|
930
|
+
|
|
931
|
+
export async function init({
|
|
932
|
+
serverContext,
|
|
933
|
+
}: {
|
|
934
|
+
serverContext: InitServerData;
|
|
935
|
+
}) {
|
|
936
|
+
// Initialize database connection
|
|
937
|
+
await connectToDatabase();
|
|
938
|
+
|
|
939
|
+
// Setup external services
|
|
940
|
+
await setupExternalServices();
|
|
941
|
+
|
|
942
|
+
// Any other initialization logic
|
|
943
|
+
console.log("Server initialized successfully");
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
**Note**: `init.server.ts` is for initializing your application services, not for configuring Loly Framework. Framework configuration goes in `loly.config.ts`.
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
## CLI Commands
|
|
952
|
+
|
|
953
|
+
```bash
|
|
954
|
+
# Development server
|
|
955
|
+
npx loly dev
|
|
956
|
+
|
|
957
|
+
# Build for production
|
|
958
|
+
npx loly build
|
|
959
|
+
|
|
960
|
+
# Start production server
|
|
961
|
+
npx loly start
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
## TypeScript Support
|
|
967
|
+
|
|
968
|
+
Loly is built with TypeScript and provides full type safety:
|
|
969
|
+
|
|
970
|
+
```tsx
|
|
971
|
+
import type {
|
|
972
|
+
ServerContext,
|
|
973
|
+
ServerLoader,
|
|
974
|
+
ApiContext,
|
|
975
|
+
WssContext,
|
|
976
|
+
RouteMiddleware,
|
|
977
|
+
ApiMiddleware,
|
|
978
|
+
GenerateStaticParams,
|
|
979
|
+
} from "@lolyjs/core";
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
---
|
|
983
|
+
|
|
984
|
+
## Production
|
|
985
|
+
|
|
986
|
+
### Build
|
|
987
|
+
|
|
988
|
+
```bash
|
|
989
|
+
npm run build
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
This generates:
|
|
993
|
+
|
|
994
|
+
- Client bundle (`.loly/client`)
|
|
995
|
+
- Static pages if using SSG (`.loly/ssg`)
|
|
996
|
+
- Server code (`.loly/server`)
|
|
997
|
+
|
|
998
|
+
### Environment Variables
|
|
999
|
+
|
|
1000
|
+
```bash
|
|
1001
|
+
PORT=3000
|
|
1002
|
+
HOST=0.0.0.0
|
|
1003
|
+
NODE_ENV=production
|
|
1004
|
+
# PUBLIC_WS_BASE_URL is optional - defaults to window.location.origin
|
|
1005
|
+
# Only set if WebSocket server is on a different domain
|
|
1006
|
+
PUBLIC_WS_BASE_URL=http://localhost:3000
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
**Note:** For WebSocket connections, `PUBLIC_WS_BASE_URL` is optional. By default, `lolySocket` uses `window.location.origin`, so you only need to set it if your WebSocket server is on a different domain than your web app.
|
|
1010
|
+
|
|
1011
|
+
---
|
|
1012
|
+
|
|
1013
|
+
## Exports
|
|
1014
|
+
|
|
1015
|
+
```tsx
|
|
1016
|
+
// Server
|
|
1017
|
+
import { startDevServer, startProdServer, buildApp } from "@lolyjs/core";
|
|
1018
|
+
|
|
1019
|
+
// Types
|
|
1020
|
+
import type {
|
|
1021
|
+
ServerContext,
|
|
1022
|
+
ServerLoader,
|
|
1023
|
+
ApiContext,
|
|
1024
|
+
WssContext,
|
|
1025
|
+
RouteMiddleware,
|
|
1026
|
+
ApiMiddleware,
|
|
1027
|
+
GenerateStaticParams,
|
|
1028
|
+
} from "@lolyjs/core";
|
|
1029
|
+
|
|
1030
|
+
// Validation
|
|
1031
|
+
import { validate, safeValidate, ValidationError } from "@lolyjs/core";
|
|
1032
|
+
|
|
1033
|
+
// Security
|
|
1034
|
+
import { sanitizeString, sanitizeObject } from "@lolyjs/core";
|
|
1035
|
+
import {
|
|
1036
|
+
createRateLimiter,
|
|
1037
|
+
defaultRateLimiter,
|
|
1038
|
+
strictRateLimiter,
|
|
1039
|
+
} from "@lolyjs/core";
|
|
1040
|
+
|
|
1041
|
+
// Logging
|
|
1042
|
+
import { logger, createModuleLogger, getRequestLogger } from "@lolyjs/core";
|
|
1043
|
+
|
|
1044
|
+
// Client
|
|
1045
|
+
import { Link } from "@lolyjs/core/components";
|
|
1046
|
+
import { lolySocket } from "@lolyjs/core/sockets";
|
|
1047
|
+
import { revalidate, revalidatePath } from "@lolyjs/core/client-cache";
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
---
|
|
1051
|
+
|
|
1052
|
+
## License
|
|
1053
|
+
|
|
1054
|
+
ISC
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
1058
|
+
## Built With
|
|
1059
|
+
|
|
1060
|
+
- [React](https://react.dev/) - UI library
|
|
1061
|
+
- [Express](https://expressjs.com/) - Web framework
|
|
1062
|
+
- [Rspack](https://rspack.dev/) - Fast bundler
|
|
1063
|
+
- [Socket.IO](https://socket.io/) - WebSocket library
|
|
1064
|
+
- [Pino](https://getpino.io/) - Fast logger
|
|
1065
|
+
- [Zod](https://zod.dev/) - Schema validation
|
|
1066
|
+
- [Helmet](https://helmetjs.github.io/) - Security headers
|
|
1067
|
+
|
|
1068
|
+
---
|
|
1069
|
+
|
|
1070
|
+
<div align="center">
|
|
1071
|
+
|
|
1072
|
+
**Made with ❤️ by the Loly team**
|
|
1073
|
+
|
|
1074
|
+
</div>
|