@lolyjs/core 0.2.0-alpha.15 → 0.2.0-alpha.16

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