@lolyjs/core 0.2.0-alpha.2 → 0.2.0-alpha.21

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.
Files changed (47) hide show
  1. package/LICENCE.md +9 -0
  2. package/README.md +1074 -761
  3. package/dist/{bootstrap-BiCQmSkx.d.mts → bootstrap-BfGTMUkj.d.mts} +19 -0
  4. package/dist/{bootstrap-BiCQmSkx.d.ts → bootstrap-BfGTMUkj.d.ts} +19 -0
  5. package/dist/cli.cjs +16997 -4416
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +17007 -4416
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.cjs +14731 -1652
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.mts +295 -57
  12. package/dist/index.d.ts +295 -57
  13. package/dist/index.js +17190 -4102
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.types-DMOO-uvF.d.mts +221 -0
  16. package/dist/index.types-DMOO-uvF.d.ts +221 -0
  17. package/dist/react/cache.cjs +107 -32
  18. package/dist/react/cache.cjs.map +1 -1
  19. package/dist/react/cache.d.mts +27 -21
  20. package/dist/react/cache.d.ts +27 -21
  21. package/dist/react/cache.js +107 -32
  22. package/dist/react/cache.js.map +1 -1
  23. package/dist/react/components.cjs +10 -8
  24. package/dist/react/components.cjs.map +1 -1
  25. package/dist/react/components.js +10 -8
  26. package/dist/react/components.js.map +1 -1
  27. package/dist/react/hooks.cjs +208 -26
  28. package/dist/react/hooks.cjs.map +1 -1
  29. package/dist/react/hooks.d.mts +75 -15
  30. package/dist/react/hooks.d.ts +75 -15
  31. package/dist/react/hooks.js +208 -26
  32. package/dist/react/hooks.js.map +1 -1
  33. package/dist/react/sockets.cjs +13 -6
  34. package/dist/react/sockets.cjs.map +1 -1
  35. package/dist/react/sockets.js +13 -6
  36. package/dist/react/sockets.js.map +1 -1
  37. package/dist/react/themes.cjs +61 -18
  38. package/dist/react/themes.cjs.map +1 -1
  39. package/dist/react/themes.js +63 -20
  40. package/dist/react/themes.js.map +1 -1
  41. package/dist/runtime.cjs +544 -111
  42. package/dist/runtime.cjs.map +1 -1
  43. package/dist/runtime.d.mts +2 -2
  44. package/dist/runtime.d.ts +2 -2
  45. package/dist/runtime.js +540 -107
  46. package/dist/runtime.js.map +1 -1
  47. 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
- [![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
-
10
- *Built with React 19, Express, Rspack, Socket.IO, and TypeScript*
11
-
12
- </div>
13
-
14
- ---
15
-
16
- ## Getting Started
17
-
18
- Create a new Loly application in seconds:
19
-
20
- ```bash
21
- npx create-loly-app mi-app
22
- ```
23
-
24
- 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).
25
-
26
- ---
27
-
28
- ## Overview
29
-
30
- 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.
31
-
32
- ### What Makes Loly Different?
33
-
34
- - 🔌 **Native WebSocket Support** - Built-in Socket.IO integration with automatic namespace routing
35
- - 🎯 **Route-Level Middlewares** - Define middlewares directly in your routes for pages and APIs
36
- - 📁 **Separation of Concerns** - Server logic in `server.hook.ts` separate from React components
37
- - 🚀 **Hybrid Rendering** - SSR, SSG, and CSR with streaming support
38
- - 🛡️ **Security First** - Built-in rate limiting, validation, sanitization, and security headers
39
- - **Performance** - Fast bundling with Rspack and optimized code splitting
40
-
41
- ---
42
-
43
- ## Quick Start
44
-
45
- ### Installation
46
-
47
- ```bash
48
- npm install @lolyjs/core react react-dom
49
- # or
50
- pnpm add @lolyjs/core react react-dom
51
- ```
52
-
53
- ### Create Your First Page
54
-
55
- ```tsx
56
- // app/page.tsx
57
- export default function Home() {
58
- return <h1>Hello, Loly!</h1>;
59
- }
60
- ```
61
-
62
- ### Add Server-Side Data
63
-
64
- ```tsx
65
- // app/page/server.hook.ts
66
- import type { ServerLoader } from "@lolyjs/core";
67
-
68
- export const getServerSideProps: ServerLoader = async (ctx) => {
69
- const data = await fetchData();
70
-
71
- return {
72
- props: { data },
73
- metadata: {
74
- title: "Home Page",
75
- description: "Welcome to Loly",
76
- },
77
- };
78
- };
79
- ```
80
-
81
- ```tsx
82
- // app/page.tsx
83
- import { usePageProps } from "@lolyjs/core/hooks";
84
-
85
- export default function Home() {
86
- const { props } = usePageProps();
87
- return <h1>{props.data}</h1>;
88
- }
89
- ```
90
-
91
- ### Start Development Server
92
-
93
- ```bash
94
- npx loly dev
95
- # Server runs on http://localhost:3000
96
- ```
97
-
98
- ---
99
-
100
- ## Key Features
101
-
102
- ### 🔌 Native WebSocket Support
103
-
104
- Loly includes built-in WebSocket support with automatic namespace routing. Define WebSocket events using the same file-based routing pattern as pages and APIs:
105
-
106
- ```tsx
107
- // app/wss/chat/events.ts
108
- import type { WssContext } from "@lolyjs/core";
109
-
110
- export const events = [
111
- {
112
- name: "connection",
113
- handler: (ctx: WssContext) => {
114
- console.log("Client connected:", ctx.socket.id);
115
- },
116
- },
117
- {
118
- name: "message",
119
- handler: (ctx: WssContext) => {
120
- const { data, actions } = ctx;
121
- // Broadcast to all clients
122
- actions.broadcast("message", {
123
- text: data.text,
124
- from: ctx.socket.id,
125
- });
126
- },
127
- },
128
- ];
129
- ```
130
-
131
- **Client-side:**
132
-
133
- ```tsx
134
- import { lolySocket } from "@lolyjs/core/sockets";
135
-
136
- const socket = lolySocket("/chat");
137
-
138
- socket.on("message", (data) => {
139
- console.log("Received:", data);
140
- });
141
-
142
- socket.emit("message", { text: "Hello!" });
143
- ```
144
-
145
- **Key Benefits:**
146
- - Automatic namespace creation from file structure
147
- - Same routing pattern as pages and APIs
148
- - Built-in broadcasting helpers (`emit`, `broadcast`, `emitTo`, `emitToClient`)
149
- - No manual configuration required
150
-
151
- ### 🎯 Route-Level Middlewares
152
-
153
- Define middlewares directly in your routes for fine-grained control:
154
-
155
- **For Pages:**
156
-
157
- ```tsx
158
- // app/dashboard/server.hook.ts
159
- import type { RouteMiddleware, ServerLoader } from "@lolyjs/core";
160
-
161
- export const beforeServerData: RouteMiddleware[] = [
162
- async (ctx, next) => {
163
- // Authentication
164
- const token = ctx.req.headers.authorization;
165
- if (!token) {
166
- ctx.res.status(401).json({ error: "Unauthorized" });
167
- return;
168
- }
169
- ctx.locals.user = await verifyToken(token);
170
- await next();
171
- },
172
- ];
173
-
174
- export const getServerSideProps: ServerLoader = async (ctx) => {
175
- const user = ctx.locals.user; // Available from middleware
176
- return { props: { user } };
177
- };
178
- ```
179
-
180
- **For API Routes:**
181
-
182
- ```tsx
183
- // app/api/protected/route.ts
184
- import type { ApiMiddleware, ApiContext } from "@lolyjs/core";
185
-
186
- // Global middleware for all methods
187
- export const beforeApi: ApiMiddleware[] = [
188
- async (ctx, next) => {
189
- // Authentication
190
- const user = await verifyUser(ctx.req);
191
- ctx.locals.user = user;
192
- await next();
193
- },
194
- ];
195
-
196
- // Method-specific middleware
197
- export const beforePOST: ApiMiddleware[] = [
198
- async (ctx, next) => {
199
- // Validation specific to POST
200
- await next();
201
- },
202
- ];
203
-
204
- export async function GET(ctx: ApiContext) {
205
- const user = ctx.locals.user;
206
- return ctx.Response({ user });
207
- }
208
- ```
209
-
210
- **Key Benefits:**
211
- - Middlewares execute before loaders/handlers
212
- - Share data via `ctx.locals`
213
- - Method-specific middlewares for APIs
214
- - Clean separation of concerns
215
-
216
- ### 📁 File-Based Routing
217
-
218
- Routes are automatically created from your file structure:
219
-
220
- | File Path | Route |
221
- |-----------|-------|
222
- | `app/page.tsx` | `/` |
223
- | `app/about/page.tsx` | `/about` |
224
- | `app/blog/[slug]/page.tsx` | `/blog/:slug` |
225
- | `app/post/[...path]/page.tsx` | `/post/*` (catch-all) |
226
-
227
- **Nested Layouts:**
228
-
229
- **⚠️ 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.
230
-
231
- ```tsx
232
- // app/layout.tsx (Root layout)
233
- export default function RootLayout({ children }) {
234
- return (
235
- <div>
236
- <nav>Navigation</nav>
237
- {children}
238
- <footer>Footer</footer>
239
- </div>
240
- );
241
- }
242
- ```
243
-
244
- ```tsx
245
- // app/blog/layout.tsx (Nested layout)
246
- export default function BlogLayout({ children }) {
247
- return (
248
- <div>
249
- <aside>Sidebar</aside>
250
- <main>{children}</main>
251
- </div>
252
- );
253
- }
254
- ```
255
-
256
- ### 🚀 Hybrid Rendering
257
-
258
- Choose the best rendering strategy for each page:
259
-
260
- **SSR (Server-Side Rendering):**
261
-
262
- ```tsx
263
- // app/posts/server.hook.ts
264
- export const dynamic = "force-dynamic" as const;
265
-
266
- export const getServerSideProps: ServerLoader = async (ctx) => {
267
- const posts = await fetchFreshPosts();
268
- return { props: { posts } };
269
- };
270
- ```
271
-
272
- **SSG (Static Site Generation):**
273
-
274
- ```tsx
275
- // app/blog/[slug]/server.hook.ts
276
- export const dynamic = "force-static" as const;
277
-
278
- export const generateStaticParams: GenerateStaticParams = async () => {
279
- const posts = await getAllPosts();
280
- return posts.map(post => ({ slug: post.slug }));
281
- };
282
-
283
- export const getServerSideProps: ServerLoader = async (ctx) => {
284
- const post = await getPost(ctx.params.slug);
285
- return { props: { post } };
286
- };
287
- ```
288
-
289
- **CSR (Client-Side Rendering):**
290
-
291
- ```tsx
292
- // app/dashboard/page.tsx (No server.hook.ts)
293
- import { useState, useEffect } from "react";
294
-
295
- export default function Dashboard() {
296
- const [data, setData] = useState(null);
297
-
298
- useEffect(() => {
299
- fetchData().then(setData);
300
- }, []);
301
-
302
- return <div>{data}</div>;
303
- }
304
- ```
305
-
306
- ### 🔌 API Routes
307
-
308
- Create RESTful APIs with flexible middleware support:
309
-
310
- ```tsx
311
- // app/api/posts/route.ts
312
- import type { ApiContext } from "@lolyjs/core";
313
- import { validate } from "@lolyjs/core";
314
- import { z } from "zod";
315
-
316
- const postSchema = z.object({
317
- title: z.string().min(1),
318
- content: z.string().min(1),
319
- });
320
-
321
- export async function GET(ctx: ApiContext) {
322
- const posts = await getPosts();
323
- return ctx.Response({ posts });
324
- }
325
-
326
- export async function POST(ctx: ApiContext) {
327
- const data = validate(postSchema, ctx.req.body);
328
- const post = await createPost(data);
329
- return ctx.Response({ post }, 201);
330
- }
331
- ```
332
-
333
- ### 🛡️ Built-in Security
334
-
335
- **Rate Limiting:**
336
-
337
- ```tsx
338
- // loly.config.ts
339
- import { ServerConfig } from "@lolyjs/core";
340
-
341
- export const config = (env: string): ServerConfig => {
342
- return {
343
- rateLimit: {
344
- windowMs: 15 * 60 * 1000, // 15 minutes
345
- max: 1000,
346
- strictMax: 5,
347
- strictPatterns: ["/api/auth/**"],
348
- },
349
- };
350
- };
351
- ```
352
-
353
- **Validation with Zod:**
354
-
355
- ```tsx
356
- import { validate, ValidationError } from "@lolyjs/core";
357
- import { z } from "zod";
358
-
359
- const schema = z.object({
360
- email: z.string().email(),
361
- age: z.number().int().min(0).max(150),
362
- });
363
-
364
- try {
365
- const data = validate(schema, req.body);
366
- } catch (error) {
367
- if (error instanceof ValidationError) {
368
- return Response({ errors: error.format() }, 400);
369
- }
370
- }
371
- ```
372
-
373
- **Automatic Sanitization:**
374
-
375
- Route parameters and query strings are automatically sanitized to prevent XSS attacks.
376
-
377
- **Security Headers:**
378
-
379
- Helmet is configured by default with CSP (Content Security Policy) and nonce support.
380
-
381
- ### 📝 Structured Logging
382
-
383
- ```tsx
384
- import { getRequestLogger, createModuleLogger } from "@lolyjs/core";
385
-
386
- // Request logger (automatic request ID)
387
- export const getServerSideProps: ServerLoader = async (ctx) => {
388
- const logger = getRequestLogger(ctx.req);
389
- logger.info("Processing request", { userId: ctx.locals.user?.id });
390
- return { props: {} };
391
- };
392
-
393
- // Module logger
394
- const logger = createModuleLogger("my-module");
395
- logger.info("Module initialized");
396
- logger.error("Error occurred", error);
397
- ```
398
-
399
- ---
400
-
401
- ## Project Structure
402
-
403
- ```
404
- your-app/
405
- ├── app/
406
- │ ├── layout.tsx # Root layout
407
- │ ├── page.tsx # Home page (/)
408
- │ ├── server.hook.ts # Server logic for home
409
- │ ├── _not-found.tsx # Custom 404
410
- │ ├── _error.tsx # Custom error page
411
- │ ├── blog/
412
- │ │ ├── layout.tsx # Blog layout
413
- │ │ ├── page.tsx # /blog
414
- │ │ └── [slug]/
415
- │ │ ├── page.tsx # /blog/:slug
416
- │ │ └── server.hook.ts # Server logic
417
- │ ├── api/
418
- │ │ └── posts/
419
- │ │ └── route.ts # /api/posts
420
- │ └── wss/
421
- │ └── chat/
422
- │ └── events.ts # WebSocket namespace /chat
423
- ├── components/ # React components
424
- ├── lib/ # Utilities
425
- ├── public/ # Static files
426
- ├── loly.config.ts # Framework configuration
427
- ├── init.server.ts # Server initialization (DB, services, etc.)
428
- └── package.json
429
- ```
430
-
431
- ---
432
-
433
- ## API Reference
434
-
435
- ### Server Loader
436
-
437
- ```tsx
438
- import type { ServerLoader } from "@lolyjs/core";
439
-
440
- export const getServerSideProps: ServerLoader = async (ctx) => {
441
- const { req, res, params, pathname, locals } = ctx;
442
-
443
- // Fetch data
444
- const data = await fetchData();
445
-
446
- // Redirect
447
- return {
448
- redirect: {
449
- destination: "/new-path",
450
- permanent: true,
451
- },
452
- };
453
-
454
- // Not found
455
- return { notFound: true };
456
-
457
- // Return props
458
- return {
459
- props: { data },
460
- metadata: {
461
- title: "Page Title",
462
- description: "Page description",
463
- },
464
- };
465
- };
466
- ```
467
-
468
- ### API Route Handler
469
-
470
- ```tsx
471
- import type { ApiContext } from "@lolyjs/core";
472
-
473
- export async function GET(ctx: ApiContext) {
474
- return ctx.Response({ data: "value" });
475
- }
476
-
477
- export async function POST(ctx: ApiContext) {
478
- return ctx.Response({ created: true }, 201);
479
- }
480
-
481
- export async function DELETE(ctx: ApiContext) {
482
- return ctx.Response({ deleted: true }, 204);
483
- }
484
- ```
485
-
486
- ### WebSocket Event Handler
487
-
488
- ```tsx
489
- import type { WssContext } from "@lolyjs/core";
490
-
491
- export const events = [
492
- {
493
- name: "connection",
494
- handler: (ctx: WssContext) => {
495
- // Handle connection
496
- },
497
- },
498
- {
499
- name: "custom-event",
500
- handler: (ctx: WssContext) => {
501
- const { socket, data, actions } = ctx;
502
-
503
- // Emit to all clients
504
- actions.emit("response", { message: "Hello" });
505
-
506
- // Broadcast to all except sender
507
- actions.broadcast("notification", data);
508
-
509
- // Emit to specific socket
510
- actions.emitTo(socketId, "private", data);
511
- },
512
- },
513
- ];
514
- ```
515
-
516
- ### Client Hooks
517
-
518
- ```tsx
519
- import { usePageProps } from "@lolyjs/core/hooks";
520
- import { revalidate } from "@lolyjs/core/client-cache";
521
-
522
- export default function Page() {
523
- const { params, props } = usePageProps();
524
-
525
- const handleRefresh = async () => {
526
- await revalidate(); // Refresh current page data
527
- };
528
-
529
- return <div>{/* Your UI */}</div>;
530
- }
531
- ```
532
-
533
- ### Components
534
-
535
- ```tsx
536
- import { Link } from "@lolyjs/core/components";
537
-
538
- export default function Navigation() {
539
- return (
540
- <nav>
541
- <Link href="/">Home</Link>
542
- <Link href="/about">About</Link>
543
- <Link href="/blog/[slug]" params={{ slug: "my-post" }}>
544
- My Post
545
- </Link>
546
- </nav>
547
- );
548
- }
549
- ```
550
-
551
- ---
552
-
553
- ## Configuration
554
-
555
- ### Framework Configuration
556
-
557
- Create `loly.config.ts` in your project root to configure the framework:
558
-
559
- ```tsx
560
- import { FrameworkConfig } from "@lolyjs/core";
561
-
562
- export default {
563
- directories: {
564
- app: "app",
565
- build: ".loly",
566
- static: "public",
567
- },
568
- server: {
569
- port: 3000,
570
- host: "localhost",
571
- },
572
- routing: {
573
- trailingSlash: "ignore",
574
- caseSensitive: false,
575
- basePath: "",
576
- },
577
- rendering: {
578
- framework: "react",
579
- streaming: true,
580
- ssr: true,
581
- ssg: true,
582
- },
583
- } satisfies FrameworkConfig;
584
- ```
585
-
586
- ### Server Configuration
587
-
588
- Configure server settings (CORS, rate limiting, etc.) in `loly.config.ts` by exporting a `config` function:
589
-
590
- ```tsx
591
- // loly.config.ts
592
- import { ServerConfig } from "@lolyjs/core";
593
-
594
- export const config = (env: string): ServerConfig => {
595
- return {
596
- bodyLimit: "1mb",
597
- corsOrigin: env === "production"
598
- ? ["https://yourdomain.com"]
599
- : "*",
600
- rateLimit: {
601
- windowMs: 15 * 60 * 1000,
602
- max: 1000,
603
- strictMax: 5,
604
- strictPatterns: ["/api/auth/**"],
605
- },
606
- };
607
- };
608
- ```
609
-
610
- ### Server Initialization
611
-
612
- Create `init.server.ts` in your project root to initialize services when Express starts (database connections, external services, etc.):
613
-
614
- ```tsx
615
- // init.server.ts
616
- import { InitServerData } from "@lolyjs/core";
617
-
618
- export async function init({
619
- serverContext,
620
- }: {
621
- serverContext: InitServerData;
622
- }) {
623
- // Initialize database connection
624
- await connectToDatabase();
625
-
626
- // Setup external services
627
- await setupExternalServices();
628
-
629
- // Any other initialization logic
630
- console.log("Server initialized successfully");
631
- }
632
- ```
633
-
634
- **Note**: `init.server.ts` is for initializing your application services, not for configuring Loly Framework. Framework configuration goes in `loly.config.ts`.
635
-
636
- ---
637
-
638
- ## CLI Commands
639
-
640
- ```bash
641
- # Development server
642
- npx loly dev
643
-
644
- # Build for production
645
- npx loly build
646
-
647
- # Start production server
648
- npx loly start
649
- ```
650
-
651
- ---
652
-
653
- ## TypeScript Support
654
-
655
- Loly is built with TypeScript and provides full type safety:
656
-
657
- ```tsx
658
- import type {
659
- ServerContext,
660
- ServerLoader,
661
- ApiContext,
662
- WssContext,
663
- RouteMiddleware,
664
- ApiMiddleware,
665
- GenerateStaticParams,
666
- } from "@lolyjs/core";
667
- ```
668
-
669
- ---
670
-
671
- ## Production
672
-
673
- ### Build
674
-
675
- ```bash
676
- npm run build
677
- ```
678
-
679
- This generates:
680
- - Client bundle (`.loly/client`)
681
- - Static pages if using SSG (`.loly/ssg`)
682
- - Server code (`.loly/server`)
683
-
684
- ### Environment Variables
685
-
686
- ```bash
687
- PORT=3000
688
- HOST=0.0.0.0
689
- NODE_ENV=production
690
- PUBLIC_WS_BASE_URL=http://localhost:3000
691
- ```
692
-
693
- ---
694
-
695
- ## Exports
696
-
697
- ```tsx
698
- // Server
699
- import { startDevServer, startProdServer, buildApp } from "@lolyjs/core";
700
-
701
- // Types
702
- import type {
703
- ServerContext,
704
- ServerLoader,
705
- ApiContext,
706
- WssContext,
707
- RouteMiddleware,
708
- ApiMiddleware,
709
- GenerateStaticParams,
710
- } from "@lolyjs/core";
711
-
712
- // Validation
713
- import { validate, safeValidate, ValidationError } from "@lolyjs/core";
714
-
715
- // Security
716
- import { sanitizeString, sanitizeObject } from "@lolyjs/core";
717
- import {
718
- createRateLimiter,
719
- defaultRateLimiter,
720
- strictRateLimiter
721
- } from "@lolyjs/core";
722
-
723
- // Logging
724
- import {
725
- logger,
726
- createModuleLogger,
727
- getRequestLogger
728
- } from "@lolyjs/core";
729
-
730
- // Client
731
- import { Link } from "@lolyjs/core/components";
732
- import { usePageProps } from "@lolyjs/core/hooks";
733
- import { lolySocket } from "@lolyjs/core/sockets";
734
- import { revalidate, revalidatePath } from "@lolyjs/core/client-cache";
735
- ```
736
-
737
- ---
738
-
739
- ## License
740
-
741
- ISC
742
-
743
- ---
744
-
745
- ## Built With
746
-
747
- - [React](https://react.dev/) - UI library
748
- - [Express](https://expressjs.com/) - Web framework
749
- - [Rspack](https://rspack.dev/) - Fast bundler
750
- - [Socket.IO](https://socket.io/) - WebSocket library
751
- - [Pino](https://getpino.io/) - Fast logger
752
- - [Zod](https://zod.dev/) - Schema validation
753
- - [Helmet](https://helmetjs.github.io/) - Security headers
754
-
755
- ---
756
-
757
- <div align="center">
758
-
759
- **Made with ❤️ by the Loly team**
760
-
761
- </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 (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>