@lolyjs/core 0.2.0-alpha.9 → 0.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +1593 -762
  2. package/dist/{bootstrap-DgvWWDim.d.mts → bootstrap-BfGTMUkj.d.mts} +12 -0
  3. package/dist/{bootstrap-DgvWWDim.d.ts → bootstrap-BfGTMUkj.d.ts} +12 -0
  4. package/dist/cli.cjs +16397 -2601
  5. package/dist/cli.cjs.map +1 -1
  6. package/dist/cli.mjs +19096 -0
  7. package/dist/cli.mjs.map +1 -0
  8. package/dist/index.cjs +17419 -3204
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.mts +323 -56
  11. package/dist/index.d.ts +323 -56
  12. package/dist/index.mjs +20236 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/index.types-Duhjyfit.d.mts +280 -0
  15. package/dist/index.types-Duhjyfit.d.ts +280 -0
  16. package/dist/react/cache.cjs +82 -32
  17. package/dist/react/cache.cjs.map +1 -1
  18. package/dist/react/cache.d.mts +29 -21
  19. package/dist/react/cache.d.ts +29 -21
  20. package/dist/react/{cache.js → cache.mjs} +84 -34
  21. package/dist/react/cache.mjs.map +1 -0
  22. package/dist/react/components.cjs +11 -12
  23. package/dist/react/components.cjs.map +1 -1
  24. package/dist/react/{components.js → components.mjs} +12 -13
  25. package/dist/react/components.mjs.map +1 -0
  26. package/dist/react/hooks.cjs +16 -6
  27. package/dist/react/hooks.cjs.map +1 -1
  28. package/dist/react/{hooks.js → hooks.mjs} +24 -14
  29. package/dist/react/hooks.mjs.map +1 -0
  30. package/dist/react/sockets.cjs +5 -6
  31. package/dist/react/sockets.cjs.map +1 -1
  32. package/dist/react/sockets.mjs +21 -0
  33. package/dist/react/sockets.mjs.map +1 -0
  34. package/dist/react/themes.cjs +61 -18
  35. package/dist/react/themes.cjs.map +1 -1
  36. package/dist/react/{themes.js → themes.mjs} +64 -21
  37. package/dist/react/themes.mjs.map +1 -0
  38. package/dist/runtime.cjs +465 -117
  39. package/dist/runtime.cjs.map +1 -1
  40. package/dist/runtime.d.mts +2 -2
  41. package/dist/runtime.d.ts +2 -2
  42. package/dist/{runtime.js → runtime.mjs} +466 -118
  43. package/dist/runtime.mjs.map +1 -0
  44. package/package.json +26 -26
  45. package/dist/cli.js +0 -5291
  46. package/dist/cli.js.map +0 -1
  47. package/dist/index.js +0 -6015
  48. package/dist/index.js.map +0 -1
  49. package/dist/react/cache.js.map +0 -1
  50. package/dist/react/components.js.map +0 -1
  51. package/dist/react/hooks.js.map +0 -1
  52. package/dist/react/sockets.js +0 -22
  53. package/dist/react/sockets.js.map +0 -1
  54. package/dist/react/themes.js.map +0 -1
  55. package/dist/runtime.js.map +0 -1
package/README.md CHANGED
@@ -1,762 +1,1593 @@
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 `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
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
- import { usePageProps } from "@lolyjs/core/hooks";
88
-
89
- export default function Home() {
90
- const { props } = usePageProps();
91
- return <h1>{props.data}</h1>;
92
- }
93
- ```
94
-
95
- ### Start Development Server
96
-
97
- ```bash
98
- npx loly dev
99
- # Server runs on http://localhost:3000
100
- ```
101
-
102
- ---
103
-
104
- ## Key Features
105
-
106
- ### 🔌 Native WebSocket Support
107
-
108
- Loly includes built-in WebSocket support with automatic namespace routing. Define WebSocket events using the same file-based routing pattern as pages and APIs:
109
-
110
- ```tsx
111
- // app/wss/chat/events.ts
112
- import type { WssContext } from "@lolyjs/core";
113
-
114
- export const events = [
115
- {
116
- name: "connection",
117
- handler: (ctx: WssContext) => {
118
- console.log("Client connected:", ctx.socket.id);
119
- },
120
- },
121
- {
122
- name: "message",
123
- handler: (ctx: WssContext) => {
124
- const { data, actions } = ctx;
125
- // Broadcast to all clients
126
- actions.broadcast("message", {
127
- text: data.text,
128
- from: ctx.socket.id,
129
- });
130
- },
131
- },
132
- ];
133
- ```
134
-
135
- **Client-side:**
136
-
137
- ```tsx
138
- import { lolySocket } from "@lolyjs/core/sockets";
139
-
140
- const socket = lolySocket("/chat");
141
-
142
- socket.on("message", (data) => {
143
- console.log("Received:", data);
144
- });
145
-
146
- socket.emit("message", { text: "Hello!" });
147
- ```
148
-
149
- **Key Benefits:**
150
-
151
- - Automatic namespace creation from file structure
152
- - Same routing pattern as pages and APIs
153
- - Built-in broadcasting helpers (`emit`, `broadcast`, `emitTo`, `emitToClient`)
154
- - No manual configuration required
155
-
156
- ### 🎯 Route-Level Middlewares
157
-
158
- Define middlewares directly in your routes for fine-grained control:
159
-
160
- **For Pages:**
161
-
162
- ```tsx
163
- // app/dashboard/server.hook.ts
164
- import type { RouteMiddleware, ServerLoader } from "@lolyjs/core";
165
-
166
- export const beforeServerData: RouteMiddleware[] = [
167
- async (ctx, next) => {
168
- // Authentication
169
- const token = ctx.req.headers.authorization;
170
- if (!token) {
171
- ctx.res.status(401).json({ error: "Unauthorized" });
172
- return;
173
- }
174
- ctx.locals.user = await verifyToken(token);
175
- await next();
176
- },
177
- ];
178
-
179
- export const getServerSideProps: ServerLoader = async (ctx) => {
180
- const user = ctx.locals.user; // Available from middleware
181
- return { props: { user } };
182
- };
183
- ```
184
-
185
- **For API Routes:**
186
-
187
- ```tsx
188
- // app/api/protected/route.ts
189
- import type { ApiMiddleware, ApiContext } from "@lolyjs/core";
190
-
191
- // Global middleware for all methods
192
- export const beforeApi: ApiMiddleware[] = [
193
- async (ctx, next) => {
194
- // Authentication
195
- const user = await verifyUser(ctx.req);
196
- ctx.locals.user = user;
197
- await next();
198
- },
199
- ];
200
-
201
- // Method-specific middleware
202
- export const beforePOST: ApiMiddleware[] = [
203
- async (ctx, next) => {
204
- // Validation specific to POST
205
- await next();
206
- },
207
- ];
208
-
209
- export async function GET(ctx: ApiContext) {
210
- const user = ctx.locals.user;
211
- return ctx.Response({ user });
212
- }
213
- ```
214
-
215
- **Key Benefits:**
216
-
217
- - Middlewares execute before loaders/handlers
218
- - Share data via `ctx.locals`
219
- - Method-specific middlewares for APIs
220
- - Clean separation of concerns
221
-
222
- ### 📁 File-Based Routing
223
-
224
- Routes are automatically created from your file structure:
225
-
226
- | File Path | Route |
227
- | ----------------------------- | --------------------- |
228
- | `app/page.tsx` | `/` |
229
- | `app/about/page.tsx` | `/about` |
230
- | `app/blog/[slug]/page.tsx` | `/blog/:slug` |
231
- | `app/post/[...path]/page.tsx` | `/post/*` (catch-all) |
232
-
233
- **Nested Layouts:**
234
-
235
- **⚠️ 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.
236
-
237
- ```tsx
238
- // app/layout.tsx (Root layout)
239
- export default function RootLayout({ children }) {
240
- return (
241
- <div>
242
- <nav>Navigation</nav>
243
- {children}
244
- <footer>Footer</footer>
245
- </div>
246
- );
247
- }
248
- ```
249
-
250
- ```tsx
251
- // app/blog/layout.tsx (Nested layout)
252
- export default function BlogLayout({ children }) {
253
- return (
254
- <div>
255
- <aside>Sidebar</aside>
256
- <main>{children}</main>
257
- </div>
258
- );
259
- }
260
- ```
261
-
262
- ### 🚀 Hybrid Rendering
263
-
264
- Choose the best rendering strategy for each page:
265
-
266
- **SSR (Server-Side Rendering):**
267
-
268
- ```tsx
269
- // app/posts/server.hook.ts
270
- export const dynamic = "force-dynamic" as const;
271
-
272
- export const getServerSideProps: ServerLoader = async (ctx) => {
273
- const posts = await fetchFreshPosts();
274
- return { props: { posts } };
275
- };
276
- ```
277
-
278
- **SSG (Static Site Generation):**
279
-
280
- ```tsx
281
- // app/blog/[slug]/server.hook.ts
282
- export const dynamic = "force-static" as const;
283
-
284
- export const generateStaticParams: GenerateStaticParams = async () => {
285
- const posts = await getAllPosts();
286
- return posts.map((post) => ({ slug: post.slug }));
287
- };
288
-
289
- export const getServerSideProps: ServerLoader = async (ctx) => {
290
- const post = await getPost(ctx.params.slug);
291
- return { props: { post } };
292
- };
293
- ```
294
-
295
- **CSR (Client-Side Rendering):**
296
-
297
- ```tsx
298
- // app/dashboard/page.tsx (No server.hook.ts)
299
- import { useState, useEffect } from "react";
300
-
301
- export default function Dashboard() {
302
- const [data, setData] = useState(null);
303
-
304
- useEffect(() => {
305
- fetchData().then(setData);
306
- }, []);
307
-
308
- return <div>{data}</div>;
309
- }
310
- ```
311
-
312
- ### 🔌 API Routes
313
-
314
- Create RESTful APIs with flexible middleware support:
315
-
316
- ```tsx
317
- // app/api/posts/route.ts
318
- import type { ApiContext } from "@lolyjs/core";
319
- import { validate } from "@lolyjs/core";
320
- import { z } from "zod";
321
-
322
- const postSchema = z.object({
323
- title: z.string().min(1),
324
- content: z.string().min(1),
325
- });
326
-
327
- export async function GET(ctx: ApiContext) {
328
- const posts = await getPosts();
329
- return ctx.Response({ posts });
330
- }
331
-
332
- export async function POST(ctx: ApiContext) {
333
- const data = validate(postSchema, ctx.req.body);
334
- const post = await createPost(data);
335
- return ctx.Response({ post }, 201);
336
- }
337
- ```
338
-
339
- ### 🛡️ Built-in Security
340
-
341
- **Rate Limiting:**
342
-
343
- ```tsx
344
- // loly.config.ts
345
- import { ServerConfig } from "@lolyjs/core";
346
-
347
- export const config = (env: string): ServerConfig => {
348
- return {
349
- rateLimit: {
350
- windowMs: 15 * 60 * 1000, // 15 minutes
351
- max: 1000,
352
- strictMax: 5,
353
- strictPatterns: ["/api/auth/**"],
354
- },
355
- };
356
- };
357
- ```
358
-
359
- **Validation with Zod:**
360
-
361
- ```tsx
362
- import { validate, ValidationError } from "@lolyjs/core";
363
- import { z } from "zod";
364
-
365
- const schema = z.object({
366
- email: z.string().email(),
367
- age: z.number().int().min(0).max(150),
368
- });
369
-
370
- try {
371
- const data = validate(schema, req.body);
372
- } catch (error) {
373
- if (error instanceof ValidationError) {
374
- return Response({ errors: error.format() }, 400);
375
- }
376
- }
377
- ```
378
-
379
- **Automatic Sanitization:**
380
-
381
- Route parameters and query strings are automatically sanitized to prevent XSS attacks.
382
-
383
- **Security Headers:**
384
-
385
- Helmet is configured by default with CSP (Content Security Policy) and nonce support.
386
-
387
- ### 📝 Structured Logging
388
-
389
- ```tsx
390
- import { getRequestLogger, createModuleLogger } from "@lolyjs/core";
391
-
392
- // Request logger (automatic request ID)
393
- export const getServerSideProps: ServerLoader = async (ctx) => {
394
- const logger = getRequestLogger(ctx.req);
395
- logger.info("Processing request", { userId: ctx.locals.user?.id });
396
- return { props: {} };
397
- };
398
-
399
- // Module logger
400
- const logger = createModuleLogger("my-module");
401
- logger.info("Module initialized");
402
- logger.error("Error occurred", error);
403
- ```
404
-
405
- ---
406
-
407
- ## Project Structure
408
-
409
- ```
410
- your-app/
411
- ├── app/
412
- │ ├── layout.tsx # Root layout
413
- │ ├── page.tsx # Home page (/)
414
- │ ├── server.hook.ts # Server logic for home
415
- │ ├── _not-found.tsx # Custom 404
416
- │ ├── _error.tsx # Custom error page
417
- │ ├── blog/
418
- │ │ ├── layout.tsx # Blog layout
419
- │ │ ├── page.tsx # /blog
420
- │ │ └── [slug]/
421
- │ │ ├── page.tsx # /blog/:slug
422
- │ │ └── server.hook.ts # Server logic
423
- │ ├── api/
424
- │ └── posts/
425
- │ └── route.ts # /api/posts
426
- │ └── wss/
427
- └── chat/
428
- └── events.ts # WebSocket namespace /chat
429
- ├── components/ # React components
430
- ├── lib/ # Utilities
431
- ├── public/ # Static files
432
- ├── loly.config.ts # Framework configuration
433
- ├── init.server.ts # Server initialization (DB, services, etc.)
434
- └── package.json
435
- ```
436
-
437
- ---
438
-
439
- ## API Reference
440
-
441
- ### Server Loader
442
-
443
- ```tsx
444
- import type { ServerLoader } from "@lolyjs/core";
445
-
446
- export const getServerSideProps: ServerLoader = async (ctx) => {
447
- const { req, res, params, pathname, locals } = ctx;
448
-
449
- // Fetch data
450
- const data = await fetchData();
451
-
452
- // Redirect
453
- return {
454
- redirect: {
455
- destination: "/new-path",
456
- permanent: true,
457
- },
458
- };
459
-
460
- // Not found
461
- return { notFound: true };
462
-
463
- // Return props
464
- return {
465
- props: { data },
466
- metadata: {
467
- title: "Page Title",
468
- description: "Page description",
469
- },
470
- };
471
- };
472
- ```
473
-
474
- ### API Route Handler
475
-
476
- ```tsx
477
- import type { ApiContext } from "@lolyjs/core";
478
-
479
- export async function GET(ctx: ApiContext) {
480
- return ctx.Response({ data: "value" });
481
- }
482
-
483
- export async function POST(ctx: ApiContext) {
484
- return ctx.Response({ created: true }, 201);
485
- }
486
-
487
- export async function DELETE(ctx: ApiContext) {
488
- return ctx.Response({ deleted: true }, 204);
489
- }
490
- ```
491
-
492
- ### WebSocket Event Handler
493
-
494
- ```tsx
495
- import type { WssContext } from "@lolyjs/core";
496
-
497
- export const events = [
498
- {
499
- name: "connection",
500
- handler: (ctx: WssContext) => {
501
- // Handle connection
502
- },
503
- },
504
- {
505
- name: "custom-event",
506
- handler: (ctx: WssContext) => {
507
- const { socket, data, actions } = ctx;
508
-
509
- // Emit to all clients
510
- actions.emit("response", { message: "Hello" });
511
-
512
- // Broadcast to all except sender
513
- actions.broadcast("notification", data);
514
-
515
- // Emit to specific socket
516
- actions.emitTo(socketId, "private", data);
517
- },
518
- },
519
- ];
520
- ```
521
-
522
- ### Client Hooks
523
-
524
- ```tsx
525
- import { usePageProps } from "@lolyjs/core/hooks";
526
- import { revalidate } from "@lolyjs/core/client-cache";
527
-
528
- export default function Page() {
529
- const { params, props } = usePageProps();
530
-
531
- const handleRefresh = async () => {
532
- await revalidate(); // Refresh current page data
533
- };
534
-
535
- return <div>{/* Your UI */}</div>;
536
- }
537
- ```
538
-
539
- ### Components
540
-
541
- ```tsx
542
- import { Link } from "@lolyjs/core/components";
543
-
544
- export default function Navigation() {
545
- return (
546
- <nav>
547
- <Link href="/">Home</Link>
548
- <Link href="/about">About</Link>
549
- <Link href="/blog/[slug]" params={{ slug: "my-post" }}>
550
- My Post
551
- </Link>
552
- </nav>
553
- );
554
- }
555
- ```
556
-
557
- ---
558
-
559
- ## Configuration
560
-
561
- ### Framework Configuration
562
-
563
- Create `loly.config.ts` in your project root to configure the framework:
564
-
565
- ```tsx
566
- import { FrameworkConfig } from "@lolyjs/core";
567
-
568
- export default {
569
- directories: {
570
- app: "app",
571
- build: ".loly",
572
- static: "public",
573
- },
574
- server: {
575
- port: 3000,
576
- host: "localhost",
577
- },
578
- routing: {
579
- trailingSlash: "ignore",
580
- caseSensitive: false,
581
- basePath: "",
582
- },
583
- rendering: {
584
- framework: "react",
585
- streaming: true,
586
- ssr: true,
587
- ssg: true,
588
- },
589
- } satisfies FrameworkConfig;
590
- ```
591
-
592
- ### Server Configuration
593
-
594
- Configure server settings (CORS, rate limiting, etc.) in `loly.config.ts` by exporting a `config` function:
595
-
596
- ```tsx
597
- // loly.config.ts
598
- import { ServerConfig } from "@lolyjs/core";
599
-
600
- export const config = (env: string): ServerConfig => {
601
- return {
602
- bodyLimit: "1mb",
603
- corsOrigin: env === "production" ? ["https://yourdomain.com"] : "*",
604
- rateLimit: {
605
- windowMs: 15 * 60 * 1000,
606
- max: 1000,
607
- strictMax: 5,
608
- strictPatterns: ["/api/auth/**"],
609
- },
610
- };
611
- };
612
- ```
613
-
614
- ### Server Initialization
615
-
616
- Create `init.server.ts` in your project root to initialize services when Express starts (database connections, external services, etc.):
617
-
618
- ```tsx
619
- // init.server.ts
620
- import { InitServerData } from "@lolyjs/core";
621
-
622
- export async function init({
623
- serverContext,
624
- }: {
625
- serverContext: InitServerData;
626
- }) {
627
- // Initialize database connection
628
- await connectToDatabase();
629
-
630
- // Setup external services
631
- await setupExternalServices();
632
-
633
- // Any other initialization logic
634
- console.log("Server initialized successfully");
635
- }
636
- ```
637
-
638
- **Note**: `init.server.ts` is for initializing your application services, not for configuring Loly Framework. Framework configuration goes in `loly.config.ts`.
639
-
640
- ---
641
-
642
- ## CLI Commands
643
-
644
- ```bash
645
- # Development server
646
- npx loly dev
647
-
648
- # Build for production
649
- npx loly build
650
-
651
- # Start production server
652
- npx loly start
653
- ```
654
-
655
- ---
656
-
657
- ## TypeScript Support
658
-
659
- Loly is built with TypeScript and provides full type safety:
660
-
661
- ```tsx
662
- import type {
663
- ServerContext,
664
- ServerLoader,
665
- ApiContext,
666
- WssContext,
667
- RouteMiddleware,
668
- ApiMiddleware,
669
- GenerateStaticParams,
670
- } from "@lolyjs/core";
671
- ```
672
-
673
- ---
674
-
675
- ## Production
676
-
677
- ### Build
678
-
679
- ```bash
680
- npm run build
681
- ```
682
-
683
- This generates:
684
-
685
- - Client bundle (`.loly/client`)
686
- - Static pages if using SSG (`.loly/ssg`)
687
- - Server code (`.loly/server`)
688
-
689
- ### Environment Variables
690
-
691
- ```bash
692
- PORT=3000
693
- HOST=0.0.0.0
694
- NODE_ENV=production
695
- PUBLIC_WS_BASE_URL=http://localhost:3000
696
- ```
697
-
698
- ---
699
-
700
- ## Exports
701
-
702
- ```tsx
703
- // Server
704
- import { startDevServer, startProdServer, buildApp } from "@lolyjs/core";
705
-
706
- // Types
707
- import type {
708
- ServerContext,
709
- ServerLoader,
710
- ApiContext,
711
- WssContext,
712
- RouteMiddleware,
713
- ApiMiddleware,
714
- GenerateStaticParams,
715
- } from "@lolyjs/core";
716
-
717
- // Validation
718
- import { validate, safeValidate, ValidationError } from "@lolyjs/core";
719
-
720
- // Security
721
- import { sanitizeString, sanitizeObject } from "@lolyjs/core";
722
- import {
723
- createRateLimiter,
724
- defaultRateLimiter,
725
- strictRateLimiter,
726
- } from "@lolyjs/core";
727
-
728
- // Logging
729
- import { logger, createModuleLogger, getRequestLogger } from "@lolyjs/core";
730
-
731
- // Client
732
- import { Link } from "@lolyjs/core/components";
733
- import { usePageProps } from "@lolyjs/core/hooks";
734
- import { lolySocket } from "@lolyjs/core/sockets";
735
- import { revalidate, revalidatePath } from "@lolyjs/core/client-cache";
736
- ```
737
-
738
- ---
739
-
740
- ## License
741
-
742
- ISC
743
-
744
- ---
745
-
746
- ## Built With
747
-
748
- - [React](https://react.dev/) - UI library
749
- - [Express](https://expressjs.com/) - Web framework
750
- - [Rspack](https://rspack.dev/) - Fast bundler
751
- - [Socket.IO](https://socket.io/) - WebSocket library
752
- - [Pino](https://getpino.io/) - Fast logger
753
- - [Zod](https://zod.dev/) - Schema validation
754
- - [Helmet](https://helmetjs.github.io/) - Security headers
755
-
756
- ---
757
-
758
- <div align="center">
759
-
760
- **Made with ❤️ by the Loly team**
761
-
762
- </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 @lolyjs/cli@latest my-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
+ - 🌐 **Global Middlewares** - Middlewares that always execute (SSR and SPA) to establish consistent context
41
+ - 📁 **Separation of Concerns** - Server logic in `page.server.hook.ts` and `layout.server.hook.ts` separate from React components
42
+ - 🚀 **Hybrid Rendering** - SSR, SSG, and CSR with streaming support
43
+ - 🛡️ **Security First** - Built-in rate limiting, validation, sanitization, and security headers
44
+ - ⚡ **Performance** - Fast bundling with Rspack and optimized code splitting
45
+ - 📦 **Full ESM Support** - Native ES modules with top-level await, dynamic imports, and `import.meta.url`
46
+
47
+ ---
48
+
49
+ ## Quick Start
50
+
51
+ ### Installation
52
+
53
+ ```bash
54
+ npm install @lolyjs/core react react-dom
55
+ # or
56
+ pnpm add @lolyjs/core react react-dom
57
+ ```
58
+
59
+ ### Create Your First Page
60
+
61
+ ```tsx
62
+ // app/page.tsx
63
+ export default function Home() {
64
+ return <h1>Hello, Loly!</h1>;
65
+ }
66
+ ```
67
+
68
+ ### Add Server-Side Data
69
+
70
+ ```tsx
71
+ // app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
72
+ import type { ServerLoader } from "@lolyjs/core";
73
+
74
+ export const getServerSideProps: ServerLoader = async (ctx) => {
75
+ const data = await fetchData();
76
+
77
+ return {
78
+ props: { data },
79
+ metadata: {
80
+ title: "Home Page",
81
+ description: "Welcome to Loly",
82
+ // See "SEO & Metadata" section below for full metadata options
83
+ },
84
+ };
85
+ };
86
+ ```
87
+
88
+ ```tsx
89
+ // app/page.tsx
90
+ export default function Home({ props }) {
91
+ return <h1>{props.data}</h1>;
92
+ }
93
+ ```
94
+
95
+ ### Start Development Server
96
+
97
+ ```bash
98
+ npx loly dev
99
+ # Server runs on http://localhost:3000
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Key Features
105
+
106
+ ### 🔌 Native WebSocket Support (Realtime v1)
107
+
108
+ 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:
109
+
110
+ ```tsx
111
+ // app/wss/chat/events.ts
112
+ import { defineWssRoute } from "@lolyjs/core";
113
+ import { z } from "zod";
114
+
115
+ export default defineWssRoute({
116
+ // Authentication hook
117
+ auth: async (ctx) => {
118
+ const token = ctx.req.headers.authorization;
119
+ return await verifyToken(token); // Returns user or null
120
+ },
121
+
122
+ // Connection hook
123
+ onConnect: (ctx) => {
124
+ console.log("User connected:", ctx.user?.id);
125
+ },
126
+
127
+ // Event handlers with validation, guards, and rate limiting
128
+ events: {
129
+ message: {
130
+ // Schema validation (Zod/Valibot)
131
+ schema: z.object({
132
+ text: z.string().min(1).max(500),
133
+ }),
134
+
135
+ // Guard (permissions check)
136
+ guard: ({ user }) => !!user, // Require authentication
137
+
138
+ // Per-event rate limiting
139
+ rateLimit: {
140
+ eventsPerSecond: 10,
141
+ burst: 20,
142
+ },
143
+
144
+ // Handler
145
+ handler: (ctx) => {
146
+ ctx.actions.broadcast("message", {
147
+ text: ctx.data.text,
148
+ from: ctx.user?.id,
149
+ });
150
+ },
151
+ },
152
+ },
153
+ });
154
+ ```
155
+
156
+ **Client-side:**
157
+
158
+ ```tsx
159
+ import { lolySocket } from "@lolyjs/core/sockets";
160
+
161
+ const socket = lolySocket("/chat");
162
+
163
+ socket.on("message", (data) => {
164
+ console.log("Received:", data);
165
+ });
166
+
167
+ socket.emit("message", { text: "Hello!" });
168
+ ```
169
+
170
+ **Key Features:**
171
+
172
+ - ✅ **Production-ready**: Auth, validation, rate limiting, logging
173
+ - ✅ **Multi-instance**: Redis adapter for horizontal scaling
174
+ - **State Store**: Shared state across instances (memory/Redis)
175
+ - ✅ **Presence**: User-to-socket mapping for targeted messaging
176
+ - ✅ **Type-safe**: Full TypeScript support
177
+ - ✅ **Automatic namespace creation** from file structure
178
+ - ✅ **Same routing pattern** as pages and APIs
179
+ - **Built-in helpers**: `emit`, `broadcast`, `toUser()`, `toRoom()`, `join()`, `leave()`
180
+ - **No manual configuration required** (works out of the box for localhost)
181
+
182
+ **📖 For complete documentation, see [REALTIME.md](./docs/REALTIME.md)**
183
+
184
+ ### 🎯 Route-Level Middlewares
185
+
186
+ 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.
187
+
188
+ **For Pages:**
189
+
190
+ ```tsx
191
+ // app/dashboard/page.server.hook.ts (preferred) or app/dashboard/server.hook.ts (legacy)
192
+ import type { RouteMiddleware, ServerLoader } from "@lolyjs/core";
193
+
194
+ export const beforeServerData: RouteMiddleware[] = [
195
+ async (ctx, next) => {
196
+ // Authentication
197
+ const token = ctx.req.headers.authorization;
198
+ if (!token) {
199
+ ctx.res.redirect("/login");
200
+ return; // Don't call next() if redirecting
201
+ }
202
+ ctx.locals.user = await verifyToken(token);
203
+ await next(); // Call next() to continue to next middleware or getServerSideProps
204
+ },
205
+ ];
206
+
207
+ export const getServerSideProps: ServerLoader = async (ctx) => {
208
+ const user = ctx.locals.user; // Available from middleware
209
+ return { props: { user } };
210
+ };
211
+ ```
212
+
213
+ **For API Routes:**
214
+
215
+ ```tsx
216
+ // app/api/protected/route.ts
217
+ import type { ApiMiddleware, ApiContext } from "@lolyjs/core";
218
+
219
+ // Global middleware for all methods (GET, POST, PUT, etc.)
220
+ export const beforeApi: ApiMiddleware[] = [
221
+ async (ctx, next) => {
222
+ // Authentication
223
+ const user = await getUser(ctx.req);
224
+ if (!user) {
225
+ return ctx.Response({ error: "Unauthorized" }, 401);
226
+ }
227
+ ctx.locals.user = user;
228
+ await next();
229
+ },
230
+ ];
231
+
232
+ // Method-specific middleware (only runs before POST)
233
+ export const beforePOST: ApiMiddleware[] = [
234
+ async (ctx, next) => {
235
+ // Validation specific to POST
236
+ await next();
237
+ },
238
+ ];
239
+
240
+ // Method-specific middleware (only runs before GET)
241
+ export const beforeGET: ApiMiddleware[] = [
242
+ async (ctx, next) => {
243
+ // Cache logic specific to GET
244
+ await next();
245
+ },
246
+ ];
247
+
248
+ export async function GET(ctx: ApiContext) {
249
+ const user = ctx.locals.user;
250
+ return ctx.Response({ user });
251
+ }
252
+
253
+ export async function POST(ctx: ApiContext) {
254
+ const user = ctx.locals.user;
255
+ const data = ctx.req.body;
256
+ return ctx.Response({ created: true }, 201);
257
+ }
258
+ ```
259
+
260
+ **Key Benefits:**
261
+
262
+ - Middlewares execute before loaders/handlers
263
+ - Share data via `ctx.locals`
264
+ - Method-specific middlewares for APIs
265
+ - Clean separation of concerns
266
+
267
+ ### 🌐 Global Middlewares
268
+
269
+ Global middlewares always execute on every request (both SSR initial load and SPA client-side navigation). They establish context (`ctx.locals`) that is consistently available across your application.
270
+
271
+ Create `global.middleware.ts` in your project root:
272
+
273
+ ```tsx
274
+ // global.middleware.ts (in project root)
275
+ import type { GlobalMiddleware } from "@lolyjs/core";
276
+
277
+ export const globalMiddlewares: GlobalMiddleware[] = [
278
+ async (ctx, next) => {
279
+ // Establish context (e.g., session, user, tenant, locale)
280
+ // This runs on EVERY request (SSR and SPA navigation)
281
+ const sessionCookie = ctx.req.cookies?.session;
282
+ if (sessionCookie) {
283
+ ctx.locals.user = await getSession(sessionCookie);
284
+ ctx.locals.session = { id: sessionCookie };
285
+ } else {
286
+ ctx.locals.user = null;
287
+ ctx.locals.session = null;
288
+ }
289
+
290
+ await next();
291
+ },
292
+ ];
293
+ ```
294
+
295
+ **Important:** Global middlewares should only establish context (set `ctx.locals`), not make routing decisions. Routing decisions should be made in layout/page hooks.
296
+
297
+ **Use Cases:**
298
+
299
+ - Session/Authentication context
300
+ - Tenant/Multi-tenancy context
301
+ - Locale/Language detection
302
+ - Feature flags based on user
303
+ - Request tracing/logging
304
+
305
+ **Example with Protected Routes:**
306
+
307
+ ```tsx
308
+ // global.middleware.ts - Establishes user context
309
+ export const globalMiddlewares: GlobalMiddleware[] = [
310
+ async (ctx, next) => {
311
+ ctx.locals.user = await getSession(ctx.req);
312
+ await next();
313
+ },
314
+ ];
315
+
316
+ // app/dashboard/layout.server.hook.ts - Makes routing decision
317
+ export const getServerSideProps: ServerLoader = async (ctx) => {
318
+ // ctx.locals.user is already available from global middleware
319
+ if (!ctx.locals.user) {
320
+ return ctx.Redirect("/login", false);
321
+ }
322
+ return { props: { user: ctx.locals.user } };
323
+ };
324
+ ```
325
+
326
+ ### 📁 File-Based Routing
327
+
328
+ Routes are automatically created from your file structure:
329
+
330
+ | File Path | Route |
331
+ | ----------------------------- | --------------------- |
332
+ | `app/page.tsx` | `/` |
333
+ | `app/about/page.tsx` | `/about` |
334
+ | `app/blog/[slug]/page.tsx` | `/blog/:slug` |
335
+ | `app/post/[...path]/page.tsx` | `/post/*` (catch-all) |
336
+
337
+ **Nested Layouts:**
338
+
339
+ **⚠️ 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.
340
+
341
+ ```tsx
342
+ // app/layout.tsx (Root layout)
343
+ export default function RootLayout({ children, appName, navigation }) {
344
+ return (
345
+ <div>
346
+ <nav>{navigation}</nav>
347
+ {children}
348
+ <footer>{appName}</footer>
349
+ </div>
350
+ );
351
+ }
352
+ ```
353
+
354
+ ```tsx
355
+ // app/layout.server.hook.ts (Root layout server hook - same directory as layout.tsx)
356
+ import type { ServerLoader } from "@lolyjs/core";
357
+
358
+ export const getServerSideProps: ServerLoader = async (ctx) => {
359
+ return {
360
+ props: {
361
+ appName: "My App",
362
+ navigation: ["Home", "About", "Blog"],
363
+ },
364
+ };
365
+ };
366
+ ```
367
+
368
+ ```tsx
369
+ // app/blog/layout.tsx (Nested layout)
370
+ export default function BlogLayout({ children, sectionTitle }) {
371
+ return (
372
+ <div>
373
+ <h1>{sectionTitle}</h1>
374
+ <aside>Sidebar</aside>
375
+ <main>{children}</main>
376
+ </div>
377
+ );
378
+ }
379
+ ```
380
+
381
+ ```tsx
382
+ // app/blog/layout.server.hook.ts (Nested layout server hook - same directory as layout.tsx)
383
+ import type { ServerLoader } from "@lolyjs/core";
384
+
385
+ export const getServerSideProps: ServerLoader = async (ctx) => {
386
+ return {
387
+ props: {
388
+ sectionTitle: "Blog Section",
389
+ },
390
+ };
391
+ };
392
+ ```
393
+
394
+ **Layout Server Hooks:**
395
+
396
+ 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:
397
+
398
+ - **Layout props** (from `layout.server.hook.ts`) are stable and available to both the layout and all pages
399
+ - **Page props** (from `page.server.hook.ts`) are specific to each page and override layout props if there's a conflict
400
+ - **Combined props** are available to both layouts and pages
401
+
402
+ **File Convention:**
403
+ - Layout server hooks: `app/layout.server.hook.ts` (same directory as `layout.tsx`)
404
+ - Page server hooks: `app/page.server.hook.ts` (preferred) or `app/server.hook.ts` (legacy, backward compatible)
405
+
406
+ ### 📦 Route Groups
407
+
408
+ Route groups allow you to organize routes without affecting the URL structure. Directories wrapped in parentheses like `(dashboard)` or `(landing)` are treated as route groups and don't appear in the URL.
409
+
410
+ **Key Features:**
411
+ - Route groups don't appear in URLs
412
+ - Each route group can have its own layout
413
+ - Route groups help organize large applications
414
+ - Layouts are applied in order: root → route group → nested → page
415
+
416
+ **Example Structure:**
417
+
418
+ ```
419
+ app/
420
+ ├── layout.tsx # Root layout (applies to all routes)
421
+ ├── layout.server.hook.ts # Root layout server hook
422
+ ├── (dashboard)/
423
+ │ ├── layout.tsx # Dashboard layout (applies to /settings, /profile)
424
+ ├── layout.server.hook.ts # Dashboard layout server hook
425
+ ├── settings/
426
+ └── page.tsx # → /settings (NOT /dashboard/settings)
427
+ └── profile/
428
+ └── page.tsx # /profile (NOT /dashboard/profile)
429
+ ├── (landing)/
430
+ ├── layout.tsx # Landing layout (applies to /about, /contact)
431
+ ├── layout.server.hook.ts # Landing layout server hook
432
+ ├── about/
433
+ │ │ └── page.tsx # /about (NOT /landing/about)
434
+ └── contact/
435
+ │ └── page.tsx # → /contact (NOT /landing/contact)
436
+ └── page.tsx # → / (root page)
437
+ ```
438
+
439
+ **Layout Order:**
440
+
441
+ For a page at `app/(dashboard)/settings/page.tsx`, layouts are applied in this order:
442
+
443
+ 1. `app/layout.tsx` (root layout)
444
+ 2. `app/(dashboard)/layout.tsx` (route group layout)
445
+ 3. `app/(dashboard)/settings/layout.tsx` (if exists, nested layout)
446
+
447
+ **Server Hooks:**
448
+
449
+ Server hooks work the same way with route groups. The execution order is:
450
+
451
+ 1. Root layout hook (`app/layout.server.hook.ts`)
452
+ 2. Route group layout hook (`app/(dashboard)/layout.server.hook.ts`)
453
+ 3. Nested layout hooks (if any)
454
+ 4. Page hook (`app/(dashboard)/settings/page.server.hook.ts`)
455
+
456
+ **Example:**
457
+
458
+ ```tsx
459
+ // app/(dashboard)/layout.tsx
460
+ export default function DashboardLayout({ children, user }) {
461
+ return (
462
+ <div className="dashboard">
463
+ <nav>Dashboard Navigation</nav>
464
+ {children}
465
+ </div>
466
+ );
467
+ }
468
+ ```
469
+
470
+ ```tsx
471
+ // app/(dashboard)/layout.server.hook.ts
472
+ import type { ServerLoader } from "@lolyjs/core";
473
+
474
+ export const getServerSideProps: ServerLoader = async (ctx) => {
475
+ const user = await getCurrentUser(ctx.req);
476
+ return {
477
+ props: {
478
+ user, // Available to all pages in (dashboard) group
479
+ },
480
+ };
481
+ };
482
+ ```
483
+
484
+ ```tsx
485
+ // app/(dashboard)/settings/page.tsx
486
+ export default function SettingsPage({ user, settings }) {
487
+ // user comes from (dashboard)/layout.server.hook.ts
488
+ // settings comes from page.server.hook.ts
489
+ return <div>Settings for {user.name}</div>;
490
+ }
491
+ ```
492
+
493
+ **Important Notes:**
494
+ - Route groups are purely organizational - they don't affect URLs
495
+ - You cannot have duplicate routes that would result from ignoring route groups
496
+ - ❌ Invalid: `app/(dashboard)/settings/page.tsx` and `app/settings/page.tsx` (both map to `/settings`)
497
+ - Valid: `app/(dashboard)/settings/page.tsx` and `app/(landing)/settings/page.tsx` (both map to `/settings` - conflict!)
498
+ - Route groups work seamlessly with SPA navigation and preloading
499
+
500
+ **Future: Parallel Routes**
501
+
502
+ The architecture is prepared for future parallel routes support (e.g., `(modal)`). Route groups can be extended to support special types that render in parallel slots.
503
+
504
+ ### 🔄 URL Rewrites
505
+
506
+ URL rewrites allow you to rewrite incoming request paths to different destination paths internally, without changing the URL visible in the browser. This is especially useful for multitenancy, API proxying, and other advanced routing scenarios.
507
+
508
+ **Key Features:**
509
+ - Rewrites happen internally - the URL in the browser doesn't change
510
+ - Support for dynamic parameters (`:param`, `*` catch-all)
511
+ - Conditional rewrites based on host, headers, cookies, or query parameters
512
+ - Async destination functions for dynamic rewrites
513
+ - High performance with pre-compiled regex patterns and caching
514
+
515
+ **Configuration:**
516
+
517
+ Create `rewrites.config.ts` in your project root:
518
+
519
+ ```typescript
520
+ import type { RewriteConfig } from "@lolyjs/core";
521
+
522
+ export default async function rewrites(): Promise<RewriteConfig> {
523
+ return [
524
+ // Static rewrite
525
+ {
526
+ source: "/old-path",
527
+ destination: "/new-path",
528
+ },
529
+
530
+ // Rewrite with parameters
531
+ {
532
+ source: "/tenant/:tenant*",
533
+ destination: "/app/:tenant*",
534
+ },
535
+
536
+ // Rewrite with async function (for dynamic logic)
537
+ {
538
+ source: "/api/proxy/:path*",
539
+ destination: async (params, req) => {
540
+ const tenant = extractTenantFromRequest(req);
541
+ return `/api/${tenant}/:path*`;
542
+ },
543
+ },
544
+
545
+ // Conditional rewrite based on host (multitenant by subdomain)
546
+ {
547
+ source: "/:path*",
548
+ has: [
549
+ { type: "host", value: ":tenant.example.com" },
550
+ ],
551
+ destination: "/project/:tenant/:path*",
552
+ },
553
+ ];
554
+ }
555
+ ```
556
+
557
+ **Multitenant by Subdomain (Main Use Case):**
558
+
559
+ The most common use case is multitenancy where each tenant has its own subdomain:
560
+
561
+ ```typescript
562
+ // rewrites.config.ts
563
+ import type { RewriteConfig } from "@lolyjs/core";
564
+
565
+ export default async function rewrites(): Promise<RewriteConfig> {
566
+ return [
567
+ // Multitenant by subdomain - catch-all pattern
568
+ // tenant1.example.com/* → /project/tenant1/*
569
+ // tenant2.example.com/* → /project/tenant2/*
570
+ // All routes under the tenant subdomain will be rewritten
571
+ // If a route doesn't exist in /project/[tenantId]/*, it will return 404
572
+ {
573
+ source: "/:path*",
574
+ has: [
575
+ {
576
+ type: "host",
577
+ value: ":tenant.example.com" // Captures tenant from subdomain
578
+ }
579
+ ],
580
+ destination: "/project/:tenant/:path*",
581
+ },
582
+ ];
583
+ }
584
+ ```
585
+
586
+ **How It Works:**
587
+ - User visits: `tenant1.example.com/dashboard`
588
+ - Internally rewrites to: `/project/tenant1/dashboard`
589
+ - URL visible in browser: `tenant1.example.com/dashboard` (unchanged)
590
+ - Route `/project/[tenantId]/dashboard` receives `params.tenantId = "tenant1"`
591
+
592
+ **Multitenant by Path:**
593
+
594
+ Alternatively, you can use path-based multitenancy:
595
+
596
+ ```typescript
597
+ // rewrites.config.ts
598
+ export default async function rewrites(): Promise<RewriteConfig> {
599
+ return [
600
+ // /tenant1/dashboard /project/tenant1/dashboard
601
+ {
602
+ source: "/:tenant/:path*",
603
+ destination: "/project/:tenant/:path*",
604
+ },
605
+ ];
606
+ }
607
+ ```
608
+
609
+ **API Proxy Example:**
610
+
611
+ ```typescript
612
+ export default async function rewrites(): Promise<RewriteConfig> {
613
+ return [
614
+ // Proxy all /api/proxy/* requests to external API
615
+ {
616
+ source: "/api/proxy/:path*",
617
+ destination: async (params, req) => {
618
+ const externalApi = process.env.EXTERNAL_API_URL;
619
+ return `${externalApi}/${params.path}`;
620
+ },
621
+ },
622
+ ];
623
+ }
624
+ ```
625
+
626
+ **Conditional Rewrites:**
627
+
628
+ Rewrites can be conditional based on request properties:
629
+
630
+ ```typescript
631
+ export default async function rewrites(): Promise<RewriteConfig> {
632
+ return [
633
+ // Rewrite based on host
634
+ {
635
+ source: "/:path*",
636
+ has: [
637
+ { type: "host", value: "api.example.com" },
638
+ ],
639
+ destination: "/api/:path*",
640
+ },
641
+
642
+ // Rewrite based on header
643
+ {
644
+ source: "/admin/:path*",
645
+ has: [
646
+ { type: "header", key: "X-Admin-Key", value: "secret" },
647
+ ],
648
+ destination: "/admin-panel/:path*",
649
+ },
650
+
651
+ // Rewrite based on cookie
652
+ {
653
+ source: "/premium/:path*",
654
+ has: [
655
+ { type: "cookie", key: "premium", value: "true" },
656
+ ],
657
+ destination: "/premium-content/:path*",
658
+ },
659
+
660
+ // Rewrite based on query parameter
661
+ {
662
+ source: "/:path*",
663
+ has: [
664
+ { type: "query", key: "version", value: "v2" },
665
+ ],
666
+ destination: "/v2/:path*",
667
+ },
668
+ ];
669
+ }
670
+ ```
671
+
672
+ **Pattern Syntax:**
673
+
674
+ - `:param` - Named parameter (matches one segment)
675
+ - `:param*` - Named catch-all (matches remaining path)
676
+ - `*` - Anonymous catch-all (matches remaining path)
677
+
678
+ **Accessing Extracted Parameters:**
679
+
680
+ Parameters extracted from rewrites (including from host conditions) are automatically available in:
681
+ - `req.query` - Query parameters
682
+ - `req.locals` - Request locals (for server hooks)
683
+ - `ctx.params` - Route parameters (if the rewritten path matches a dynamic route)
684
+
685
+ ```typescript
686
+ // app/project/[tenantId]/dashboard/page.server.hook.ts
687
+ export const getServerSideProps: ServerLoader = async (ctx) => {
688
+ // tenantId comes from the rewrite: /project/:tenant/:path*
689
+ const tenantId = ctx.params.tenantId;
690
+
691
+ // Also available in req.query and req.locals
692
+ const tenantFromQuery = ctx.req.query.tenant;
693
+ const tenantFromLocals = ctx.req.locals?.tenant;
694
+
695
+ return { props: { tenantId } };
696
+ };
697
+ ```
698
+
699
+ **Performance & Caching:**
700
+
701
+ - Rewrites config is loaded once and cached
702
+ - Regex patterns are pre-compiled for performance
703
+ - In development: File tracking invalidates cache only when `rewrites.config.ts` changes
704
+ - In production: Rewrites are loaded from manifest (faster, no async functions)
705
+
706
+ **Important Notes:**
707
+
708
+ - Rewrites are applied **before** route matching
709
+ - The original URL is preserved in the browser (not a redirect)
710
+ - Query parameters are preserved and can be extended
711
+ - Rewrites work for both pages and API routes
712
+ - Functions in rewrite destinations cannot be serialized in production builds (only static rewrites are included in manifest)
713
+ - Rewrites are evaluated in order - the first match wins
714
+ - **Behavior**: Rewrites are applied ALWAYS if the source pattern matches, regardless of whether the destination route exists
715
+ - If a rewritten route doesn't exist, a 404 will be returned (strict behavior, no fallback to original route)
716
+ - Catch-all patterns (`/:path*`) are fully supported and recommended for multitenancy scenarios
717
+ - **API Routes**: Can be rewritten. If rewritten route starts with `/api/`, it's handled as API route. Otherwise, it's handled as a page route
718
+ - **WSS Routes**: Automatically excluded from rewrites (WebSocket handled separately by Socket.IO)
719
+ - System routes (`/static/*`, `/__fw/*`, `/favicon.ico`) are automatically excluded from rewrites
720
+
721
+ **Validation:**
722
+
723
+ The framework automatically validates rewrites to prevent:
724
+ - Infinite loops (warns if source and destination are identical)
725
+ - Duplicate source patterns (warns if multiple rewrites have the same source)
726
+
727
+ ### 🚀 Hybrid Rendering
728
+
729
+ Choose the best rendering strategy for each page:
730
+
731
+ **SSR (Server-Side Rendering):**
732
+
733
+ ```tsx
734
+ // app/posts/page.server.hook.ts (preferred) or app/posts/server.hook.ts (legacy)
735
+ export const dynamic = "force-dynamic" as const;
736
+
737
+ export const getServerSideProps: ServerLoader = async (ctx) => {
738
+ const posts = await fetchFreshPosts();
739
+ return { props: { posts } };
740
+ };
741
+ ```
742
+
743
+ **SSG (Static Site Generation):**
744
+
745
+ ```tsx
746
+ // app/blog/[slug]/page.server.hook.ts (preferred) or app/blog/[slug]/server.hook.ts (legacy)
747
+ export const dynamic = "force-static" as const;
748
+
749
+ export const generateStaticParams: GenerateStaticParams = async () => {
750
+ const posts = await getAllPosts();
751
+ return posts.map((post) => ({ slug: post.slug }));
752
+ };
753
+
754
+ export const getServerSideProps: ServerLoader = async (ctx) => {
755
+ const post = await getPost(ctx.params.slug);
756
+ return { props: { post } };
757
+ };
758
+ ```
759
+
760
+ **CSR (Client-Side Rendering):**
761
+
762
+ ```tsx
763
+ // app/dashboard/page.tsx (No page.server.hook.ts)
764
+ import { useState, useEffect } from "react";
765
+
766
+ export default function Dashboard() {
767
+ const [data, setData] = useState(null);
768
+
769
+ useEffect(() => {
770
+ fetchData().then(setData);
771
+ }, []);
772
+
773
+ return <div>{data}</div>;
774
+ }
775
+ ```
776
+
777
+ ### 📄 Static Files & Assets
778
+
779
+ Loly serves static files from the `public/` directory at the root of your application. This is perfect for SEO files like `sitemap.xml`, `robots.txt`, favicons, and other static assets.
780
+
781
+ **How it works:**
782
+ - Files in `public/` are served at the root URL (e.g., `public/sitemap.xml` → `/sitemap.xml`)
783
+ - Static files have **priority over dynamic routes** - if a file exists in `public/`, it will be served instead of matching a route
784
+ - Perfect for SEO: Google automatically finds `sitemap.xml` and `robots.txt` at the root
785
+ - Works in both development and production environments
786
+ - Subdirectories are supported: `public/assets/logo.png` → `/assets/logo.png`
787
+
788
+ **Directory Structure:**
789
+ ```
790
+ public/
791
+ ├── sitemap.xml # Available at /sitemap.xml
792
+ ├── robots.txt # Available at /robots.txt
793
+ ├── favicon.ico # Available at /favicon.ico (or favicon.png)
794
+ ├── favicon.png # Available at /favicon.png (alternative to .ico)
795
+ └── assets/
796
+ ├── logo.png # Available at /assets/logo.png
797
+ └── images/ # Available at /assets/images/*
798
+ └── hero.jpg
799
+ ```
800
+
801
+ **Favicon:**
802
+ Place your favicon in the `public/` directory as either `favicon.ico` or `favicon.png`. The framework automatically detects and includes it in the HTML head with the correct MIME type:
803
+ - `public/favicon.ico` → `/favicon.ico` (type: `image/x-icon`)
804
+ - `public/favicon.png` → `/favicon.png` (type: `image/png`)
805
+
806
+ If both exist, `favicon.ico` takes priority (checked first).
807
+
808
+ **SEO Example:**
809
+
810
+ Create `public/sitemap.xml`:
811
+ ```xml
812
+ <?xml version="1.0" encoding="UTF-8"?>
813
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
814
+ <url>
815
+ <loc>https://example.com/</loc>
816
+ <lastmod>2024-01-01</lastmod>
817
+ <changefreq>daily</changefreq>
818
+ <priority>1.0</priority>
819
+ </url>
820
+ </urlset>
821
+ ```
822
+
823
+ Create `public/robots.txt`:
824
+ ```
825
+ User-agent: *
826
+ Allow: /
827
+
828
+ Sitemap: https://example.com/sitemap.xml
829
+ ```
830
+
831
+ Both files will be automatically available at `/sitemap.xml` and `/robots.txt` respectively, and search engines will find them at the standard locations.
832
+
833
+ **Important Notes:**
834
+ - **All static files** (including favicons) must be placed in the `public/` directory
835
+ - The framework **only** looks for favicons in `public/` (not in the root or `app/` directory)
836
+ - Favicons are automatically detected and included in the HTML `<head>` with the correct MIME type
837
+ - Static files have **priority over dynamic routes** - perfect for SEO files
838
+
839
+ **Configuration:**
840
+ The static directory can be customized in `loly.config.ts`:
841
+ ```tsx
842
+ import type { FrameworkConfig } from "@lolyjs/core";
843
+
844
+ export default {
845
+ directories: {
846
+ static: "public", // Default: "public"
847
+ },
848
+ } satisfies Partial<FrameworkConfig>;
849
+ ```
850
+
851
+ ### 🔌 API Routes
852
+
853
+ Create RESTful APIs with flexible middleware support:
854
+
855
+ ```tsx
856
+ // app/api/posts/route.ts
857
+ import type { ApiContext } from "@lolyjs/core";
858
+ import { validate } from "@lolyjs/core";
859
+ import { z } from "zod";
860
+
861
+ const postSchema = z.object({
862
+ title: z.string().min(1),
863
+ content: z.string().min(1),
864
+ });
865
+
866
+ export async function GET(ctx: ApiContext) {
867
+ const posts = await getPosts();
868
+ return ctx.Response({ posts });
869
+ }
870
+
871
+ export async function POST(ctx: ApiContext) {
872
+ const data = validate(postSchema, ctx.req.body);
873
+ const post = await createPost(data);
874
+ return ctx.Response({ post }, 201);
875
+ }
876
+ ```
877
+
878
+ ### 📊 SEO & Metadata
879
+
880
+ Loly provides comprehensive metadata support for SEO and social sharing. Metadata can be defined at both layout and page levels, with intelligent merging:
881
+
882
+ **Layout Metadata (Base/Defaults):**
883
+
884
+ ```tsx
885
+ // app/layout.server.hook.ts
886
+ import type { ServerLoader } from "@lolyjs/core";
887
+
888
+ export const getServerSideProps: ServerLoader = async () => {
889
+ return {
890
+ props: { /* ... */ },
891
+ metadata: {
892
+ // Site-wide defaults
893
+ description: "My awesome site",
894
+ lang: "en",
895
+ robots: "index, follow",
896
+ themeColor: "#000000",
897
+
898
+ // Open Graph defaults
899
+ openGraph: {
900
+ type: "website",
901
+ siteName: "My Site",
902
+ locale: "en_US",
903
+ },
904
+
905
+ // Twitter Card defaults
906
+ twitter: {
907
+ card: "summary_large_image",
908
+ },
909
+
910
+ // Custom meta tags
911
+ metaTags: [
912
+ { name: "author", content: "My Name" },
913
+ ],
914
+
915
+ // Custom link tags (preconnect, etc.)
916
+ links: [
917
+ { rel: "preconnect", href: "https://api.example.com" },
918
+ ],
919
+ },
920
+ };
921
+ };
922
+ ```
923
+
924
+ **Page Metadata (Overrides Layout):**
925
+
926
+ ```tsx
927
+ // app/page.server.hook.ts
928
+ import type { ServerLoader } from "@lolyjs/core";
929
+
930
+ export const getServerSideProps: ServerLoader = async (ctx) => {
931
+ const post = await getPost(ctx.params.slug);
932
+
933
+ return {
934
+ props: { post },
935
+ metadata: {
936
+ // Page-specific (overrides layout)
937
+ title: `${post.title} | My Site`,
938
+ description: post.excerpt,
939
+ canonical: `https://mysite.com/blog/${post.slug}`,
940
+
941
+ // Open Graph (inherits type, siteName from layout)
942
+ openGraph: {
943
+ title: post.title,
944
+ description: post.excerpt,
945
+ url: `https://mysite.com/blog/${post.slug}`,
946
+ image: {
947
+ url: post.imageUrl,
948
+ width: 1200,
949
+ height: 630,
950
+ alt: post.title,
951
+ },
952
+ },
953
+
954
+ // Twitter Card (inherits card type from layout)
955
+ twitter: {
956
+ title: post.title,
957
+ description: post.excerpt,
958
+ image: post.imageUrl,
959
+ imageAlt: post.title,
960
+ },
961
+ },
962
+ };
963
+ };
964
+ ```
965
+
966
+ **Full Metadata API:**
967
+
968
+ ```tsx
969
+ interface PageMetadata {
970
+ // Basic fields
971
+ title?: string;
972
+ description?: string;
973
+ lang?: string;
974
+ canonical?: string;
975
+ robots?: string;
976
+ themeColor?: string;
977
+ viewport?: string;
978
+
979
+ // Open Graph
980
+ openGraph?: {
981
+ title?: string;
982
+ description?: string;
983
+ type?: string;
984
+ url?: string;
985
+ image?: string | {
986
+ url: string;
987
+ width?: number;
988
+ height?: number;
989
+ alt?: string;
990
+ };
991
+ siteName?: string;
992
+ locale?: string;
993
+ };
994
+
995
+ // Twitter Cards
996
+ twitter?: {
997
+ card?: "summary" | "summary_large_image" | "app" | "player";
998
+ title?: string;
999
+ description?: string;
1000
+ image?: string;
1001
+ imageAlt?: string;
1002
+ site?: string;
1003
+ creator?: string;
1004
+ };
1005
+
1006
+ // Custom meta tags
1007
+ metaTags?: Array<{
1008
+ name?: string;
1009
+ property?: string;
1010
+ httpEquiv?: string;
1011
+ content: string;
1012
+ }>;
1013
+
1014
+ // Custom link tags
1015
+ links?: Array<{
1016
+ rel: string;
1017
+ href: string;
1018
+ as?: string;
1019
+ crossorigin?: string;
1020
+ type?: string;
1021
+ }>;
1022
+ }
1023
+ ```
1024
+
1025
+ **Key Features:**
1026
+
1027
+ - **Layout + Page Merging**: Layout metadata provides defaults, page metadata overrides specific fields
1028
+ - **Automatic Updates**: Metadata updates automatically during SPA navigation
1029
+ - **SSR & SSG Support**: Works in both server-side rendering and static generation
1030
+ - **Type-Safe**: Full TypeScript support with `PageMetadata` type
1031
+
1032
+ ### 🛡️ Built-in Security
1033
+
1034
+ **Rate Limiting:**
1035
+
1036
+ ```tsx
1037
+ // loly.config.ts
1038
+ import { ServerConfig } from "@lolyjs/core";
1039
+
1040
+ export const config = (env: string): ServerConfig => {
1041
+ return {
1042
+ rateLimit: {
1043
+ windowMs: 15 * 60 * 1000, // 15 minutes
1044
+ max: 1000,
1045
+ strictMax: 5,
1046
+ strictPatterns: ["/api/auth/**"],
1047
+ },
1048
+ };
1049
+ };
1050
+ ```
1051
+
1052
+ **Validation with Zod:**
1053
+
1054
+ ```tsx
1055
+ import { validate, ValidationError } from "@lolyjs/core";
1056
+ import { z } from "zod";
1057
+
1058
+ const schema = z.object({
1059
+ email: z.string().email(),
1060
+ age: z.number().int().min(0).max(150),
1061
+ });
1062
+
1063
+ try {
1064
+ const data = validate(schema, req.body);
1065
+ } catch (error) {
1066
+ if (error instanceof ValidationError) {
1067
+ return Response({ errors: error.format() }, 400);
1068
+ }
1069
+ }
1070
+ ```
1071
+
1072
+ **Automatic Sanitization:**
1073
+
1074
+ Route parameters and query strings are automatically sanitized to prevent XSS attacks.
1075
+
1076
+ **Security Headers:**
1077
+
1078
+ Helmet is configured by default with CSP (Content Security Policy) and nonce support.
1079
+
1080
+ ### 📝 Structured Logging
1081
+
1082
+ ```tsx
1083
+ import { getRequestLogger, createModuleLogger } from "@lolyjs/core";
1084
+
1085
+ // Request logger (automatic request ID)
1086
+ export const getServerSideProps: ServerLoader = async (ctx) => {
1087
+ const logger = getRequestLogger(ctx.req);
1088
+ logger.info("Processing request", { userId: ctx.locals.user?.id });
1089
+ return { props: {} };
1090
+ };
1091
+
1092
+ // Module logger
1093
+ const logger = createModuleLogger("my-module");
1094
+ logger.info("Module initialized");
1095
+ logger.error("Error occurred", error);
1096
+ ```
1097
+
1098
+ ---
1099
+
1100
+ ## Project Structure
1101
+
1102
+ ```
1103
+ your-app/
1104
+ ├── app/
1105
+ │ ├── layout.tsx # Root layout
1106
+ │ ├── layout.server.hook.ts # Root layout server hook (stable props)
1107
+ │ ├── page.tsx # Home page (/)
1108
+ │ ├── page.server.hook.ts # Page server hook (preferred) or server.hook.ts (legacy)
1109
+ │ ├── _not-found.tsx # Custom 404
1110
+ │ ├── _error.tsx # Custom error page
1111
+ │ ├── blog/
1112
+ │ │ ├── layout.tsx # Blog layout
1113
+ │ │ ├── layout.server.hook.ts # Blog layout server hook
1114
+ │ │ ├── page.tsx # /blog
1115
+ │ │ └── [slug]/
1116
+ │ │ ├── page.tsx # /blog/:slug
1117
+ │ │ └── page.server.hook.ts # Page server hook
1118
+ │ ├── api/
1119
+ │ │ └── posts/
1120
+ │ │ └── route.ts # /api/posts
1121
+ │ └── wss/
1122
+ │ └── chat/
1123
+ │ └── events.ts # WebSocket namespace /chat
1124
+ ├── components/ # React components
1125
+ ├── lib/ # Utilities
1126
+ ├── public/ # Static files (served at root: /sitemap.xml, /robots.txt, etc.)
1127
+ ├── loly.config.ts # Framework configuration
1128
+ ├── global.middleware.ts # Global middlewares (always execute)
1129
+ ├── init.server.ts # Server initialization (DB, services, etc.)
1130
+ └── package.json
1131
+ ```
1132
+
1133
+ ---
1134
+
1135
+ ## API Reference
1136
+
1137
+ ### Server Loader
1138
+
1139
+ **Page Server Hook:**
1140
+
1141
+ ```tsx
1142
+ // app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
1143
+ import type { ServerLoader } from "@lolyjs/core";
1144
+
1145
+ export const getServerSideProps: ServerLoader = async (ctx) => {
1146
+ const { req, res, params, pathname, locals } = ctx;
1147
+
1148
+ // Fetch data
1149
+ const data = await fetchData();
1150
+
1151
+ // Redirect
1152
+ if (someCondition) {
1153
+ return ctx.Redirect("/new-path", true); // permanent redirect
1154
+ }
1155
+
1156
+ // Not found
1157
+ if (!data) {
1158
+ return ctx.NotFound();
1159
+ }
1160
+
1161
+ // Return props
1162
+ return {
1163
+ props: { data },
1164
+ metadata: {
1165
+ title: "Page Title",
1166
+ description: "Page description",
1167
+ // See "SEO & Metadata" section above for full metadata options
1168
+ // including Open Graph, Twitter Cards, canonical URLs, etc.
1169
+ },
1170
+ };
1171
+ };
1172
+ ```
1173
+
1174
+ **Layout Server Hook:**
1175
+
1176
+ ```tsx
1177
+ // app/layout.server.hook.ts (same directory as layout.tsx)
1178
+ import type { ServerLoader } from "@lolyjs/core";
1179
+
1180
+ export const getServerSideProps: ServerLoader = async (ctx) => {
1181
+ // Fetch stable data that persists across all pages
1182
+ const user = await getCurrentUser();
1183
+ const navigation = await getNavigation();
1184
+
1185
+ return {
1186
+ props: {
1187
+ user, // Available to layout and all pages
1188
+ navigation, // Available to layout and all pages
1189
+ },
1190
+ };
1191
+ };
1192
+ ```
1193
+
1194
+ **Props Merging:**
1195
+
1196
+ - Layout props (from `layout.server.hook.ts`) are merged first
1197
+ - Page props (from `page.server.hook.ts`) are merged second and override layout props
1198
+ - Both layouts and pages receive the combined props
1199
+
1200
+ ```tsx
1201
+ // app/layout.tsx
1202
+ export default function Layout({ user, navigation, children }) {
1203
+ // Receives: user, navigation (from layout.server.hook.ts)
1204
+ // Also receives: any props from page.server.hook.ts
1205
+ return <div>{/* ... */}</div>;
1206
+ }
1207
+
1208
+ // app/page.tsx
1209
+ export default function Page({ user, navigation, posts }) {
1210
+ // Receives: user, navigation (from layout.server.hook.ts)
1211
+ // Receives: posts (from page.server.hook.ts)
1212
+ return <div>{/* ... */}</div>;
1213
+ }
1214
+ ```
1215
+
1216
+ ### API Route Handler
1217
+
1218
+ ```tsx
1219
+ import type { ApiContext } from "@lolyjs/core";
1220
+
1221
+ export async function GET(ctx: ApiContext) {
1222
+ return ctx.Response({ data: "value" });
1223
+ }
1224
+
1225
+ export async function POST(ctx: ApiContext) {
1226
+ return ctx.Response({ created: true }, 201);
1227
+ }
1228
+
1229
+ export async function DELETE(ctx: ApiContext) {
1230
+ return ctx.Response({ deleted: true }, 204);
1231
+ }
1232
+ ```
1233
+
1234
+ ### WebSocket Event Handler (New API - Realtime v1)
1235
+
1236
+ ```tsx
1237
+ import { defineWssRoute } from "@lolyjs/core";
1238
+ import { z } from "zod";
1239
+
1240
+ export default defineWssRoute({
1241
+ auth: async (ctx) => {
1242
+ // Authenticate user
1243
+ return await getUserFromToken(ctx.req.headers.authorization);
1244
+ },
1245
+
1246
+ onConnect: (ctx) => {
1247
+ console.log("User connected:", ctx.user?.id);
1248
+ },
1249
+
1250
+ events: {
1251
+ "custom-event": {
1252
+ schema: z.object({ message: z.string() }),
1253
+ guard: ({ user }) => !!user,
1254
+ handler: (ctx) => {
1255
+ // Emit to all clients
1256
+ ctx.actions.emit("response", { message: "Hello" });
1257
+
1258
+ // Broadcast to all except sender
1259
+ ctx.actions.broadcast("notification", ctx.data);
1260
+
1261
+ // Send to specific user
1262
+ ctx.actions.toUser(userId).emit("private", ctx.data);
1263
+
1264
+ // Send to room
1265
+ ctx.actions.toRoom("room-name").emit("room-message", ctx.data);
1266
+ },
1267
+ },
1268
+ },
1269
+ });
1270
+ ```
1271
+ ```
1272
+
1273
+ ### Client Cache
1274
+
1275
+ ```tsx
1276
+ import { revalidate } from "@lolyjs/core/client-cache";
1277
+
1278
+ export default function Page({ props }) {
1279
+ const handleRefresh = async () => {
1280
+ await revalidate(); // Refresh current page data
1281
+ };
1282
+
1283
+ return <div>{/* Your UI */}</div>;
1284
+ }
1285
+ ```
1286
+
1287
+ ### Components
1288
+
1289
+ ```tsx
1290
+ import { Link } from "@lolyjs/core/components";
1291
+
1292
+ export default function Navigation() {
1293
+ return (
1294
+ <nav>
1295
+ <Link href="/">Home</Link>
1296
+ <Link href="/about">About</Link>
1297
+ <Link href="/blog/[slug]" params={{ slug: "my-post" }}>
1298
+ My Post
1299
+ </Link>
1300
+ </nav>
1301
+ );
1302
+ }
1303
+ ```
1304
+
1305
+ ---
1306
+
1307
+ ## Configuration
1308
+
1309
+ ### Framework Configuration
1310
+
1311
+ Create `loly.config.ts` in your project root to configure the framework:
1312
+
1313
+ ```tsx
1314
+ import type { FrameworkConfig } from "@lolyjs/core";
1315
+
1316
+ // Option 1: Partial config (only specify what you want to change)
1317
+ export default {
1318
+ directories: {
1319
+ static: "public",
1320
+ },
1321
+ } satisfies Partial<FrameworkConfig>;
1322
+
1323
+ // Option 2: Full config (for strict validation)
1324
+ // export default {
1325
+ // directories: { app: "app", build: ".loly", static: "public" },
1326
+ // conventions: { /* ... */ },
1327
+ // routing: { /* ... */ },
1328
+ // build: { /* ... */ },
1329
+ // server: { adapter: "express", port: 3000, host: "localhost" },
1330
+ // rendering: { framework: "react", streaming: true, ssr: true, ssg: true },
1331
+ // } satisfies FrameworkConfig;
1332
+ ```
1333
+
1334
+ ### Server Configuration
1335
+
1336
+ Configure server settings (CORS, rate limiting, WebSocket, etc.) in `loly.config.ts` by exporting a `config` function:
1337
+
1338
+ ```tsx
1339
+ // loly.config.ts
1340
+ import { ServerConfig } from "@lolyjs/core";
1341
+
1342
+ export const config = (env: string): ServerConfig => {
1343
+ const isDev = env === "development";
1344
+
1345
+ return {
1346
+ bodyLimit: "1mb",
1347
+ corsOrigin: isDev ? "*" : ["https://yourdomain.com"],
1348
+ rateLimit: {
1349
+ windowMs: 15 * 60 * 1000,
1350
+ max: 1000,
1351
+ strictMax: 5,
1352
+ strictPatterns: ["/api/auth/**"],
1353
+ },
1354
+ // Realtime (WebSocket) configuration
1355
+ realtime: {
1356
+ enabled: true,
1357
+ // For production, configure allowed origins
1358
+ // For development, localhost is auto-allowed
1359
+ allowedOrigins: isDev ? undefined : ["https://yourdomain.com"],
1360
+ // Optional: Configure Redis for multi-instance scaling
1361
+ // scale: {
1362
+ // mode: "cluster",
1363
+ // adapter: { url: "redis://localhost:6379" },
1364
+ // stateStore: { name: "redis", url: "redis://localhost:6379" },
1365
+ // },
1366
+ },
1367
+ };
1368
+ };
1369
+ ```
1370
+
1371
+ **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.
1372
+
1373
+ ### Server Initialization
1374
+
1375
+ Create `init.server.ts` in your project root to initialize services when Express starts (database connections, external services, etc.):
1376
+
1377
+ ```tsx
1378
+ // init.server.ts
1379
+ import { InitServerData } from "@lolyjs/core";
1380
+
1381
+ export async function init({
1382
+ serverContext,
1383
+ }: {
1384
+ serverContext: InitServerData;
1385
+ }) {
1386
+ // Initialize database connection
1387
+ await connectToDatabase();
1388
+
1389
+ // Setup external services
1390
+ await setupExternalServices();
1391
+
1392
+ // Any other initialization logic
1393
+ console.log("Server initialized successfully");
1394
+ }
1395
+ ```
1396
+
1397
+ **Note**: `init.server.ts` is for initializing your application services, not for configuring Loly Framework. Framework configuration goes in `loly.config.ts`.
1398
+
1399
+ ---
1400
+
1401
+ ## CLI Commands
1402
+
1403
+ ```bash
1404
+ # Development server
1405
+ npx loly dev
1406
+
1407
+ # Build for production
1408
+ npx loly build
1409
+
1410
+ # Start production server
1411
+ npx loly start
1412
+ ```
1413
+
1414
+ ---
1415
+
1416
+ ## TypeScript Support
1417
+
1418
+ Loly is built with TypeScript and provides full type safety:
1419
+
1420
+ ```tsx
1421
+ import type {
1422
+ ServerContext,
1423
+ ServerLoader,
1424
+ ApiContext,
1425
+ WssContext,
1426
+ RouteMiddleware,
1427
+ GlobalMiddleware,
1428
+ ApiMiddleware,
1429
+ GenerateStaticParams,
1430
+ } from "@lolyjs/core";
1431
+ ```
1432
+
1433
+ ---
1434
+
1435
+ ## ES Modules (ESM) Support
1436
+
1437
+ Loly Framework uses native ES modules (ESM) throughout the codebase, not CommonJS. This enables modern JavaScript features that weren't available in CJS:
1438
+
1439
+ ### Top-Level Await
1440
+
1441
+ Use `await` at the module level without wrapping in an async function:
1442
+
1443
+ ```tsx
1444
+ // app/lib/config.ts
1445
+ const config = await loadConfigFromAPI();
1446
+ export default config;
1447
+ ```
1448
+
1449
+ ### Dynamic Imports
1450
+
1451
+ Use `import()` for conditional and parallel module loading:
1452
+
1453
+ ```tsx
1454
+ // app/page.server.hook.ts
1455
+ export const getServerSideProps: ServerLoader = async () => {
1456
+ // Dynamic import - loads only when needed
1457
+ const utils = await import("../../lib/utils");
1458
+
1459
+ // Parallel imports
1460
+ const [config, helpers] = await Promise.all([
1461
+ import("../../lib/config"),
1462
+ import("../../lib/helpers"),
1463
+ ]);
1464
+
1465
+ return { props: { utils, config, helpers } };
1466
+ };
1467
+ ```
1468
+
1469
+ ### import.meta.url
1470
+
1471
+ Access module metadata for file operations (no `__dirname` in ESM):
1472
+
1473
+ ```tsx
1474
+ // app/api/files/route.ts
1475
+ import { fileURLToPath } from "url";
1476
+ import { dirname, join } from "path";
1477
+ import { readFile } from "fs/promises";
1478
+
1479
+ const __filename = fileURLToPath(import.meta.url);
1480
+ const __dirname = dirname(__filename);
1481
+
1482
+ export async function GET(ctx: ApiContext) {
1483
+ const filePath = join(__dirname, "data.json");
1484
+ const content = await readFile(filePath, "utf-8");
1485
+ return ctx.Response({ data: JSON.parse(content) });
1486
+ }
1487
+ ```
1488
+
1489
+ ### Static Files During Build
1490
+
1491
+ Static files (JSON, txt, etc.) in your `app/` directory are automatically copied to the output directory during build, preserving directory structure. This ensures files referenced via `import.meta.url` are available at runtime.
1492
+
1493
+ **Key Benefits:**
1494
+ - ✅ Native ESM - No CommonJS compilation overhead
1495
+ - ✅ Top-level await for async module initialization
1496
+ - ✅ Better tree-shaking and optimizations
1497
+ - ✅ Modern JavaScript features
1498
+ - ✅ Path aliases work correctly with ESM
1499
+
1500
+ ---
1501
+
1502
+ ## Production
1503
+
1504
+ ### Build
1505
+
1506
+ ```bash
1507
+ npm run build
1508
+ ```
1509
+
1510
+ This generates:
1511
+
1512
+ - Client bundle (`.loly/client`)
1513
+ - Static pages if using SSG (`.loly/ssg`)
1514
+ - Server code (`.loly/server`)
1515
+
1516
+ ### Environment Variables
1517
+
1518
+ ```bash
1519
+ PORT=3000
1520
+ HOST=0.0.0.0
1521
+ NODE_ENV=production
1522
+ # PUBLIC_WS_BASE_URL is optional - defaults to window.location.origin
1523
+ # Only set if WebSocket server is on a different domain
1524
+ PUBLIC_WS_BASE_URL=http://localhost:3000
1525
+ ```
1526
+
1527
+ **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.
1528
+
1529
+ ---
1530
+
1531
+ ## Exports
1532
+
1533
+ ```tsx
1534
+ // Server
1535
+ import { startDevServer, startProdServer, buildApp } from "@lolyjs/core";
1536
+
1537
+ // Types
1538
+ import type {
1539
+ ServerContext,
1540
+ ServerLoader,
1541
+ ApiContext,
1542
+ WssContext,
1543
+ RouteMiddleware,
1544
+ GlobalMiddleware,
1545
+ ApiMiddleware,
1546
+ GenerateStaticParams,
1547
+ } from "@lolyjs/core";
1548
+
1549
+ // Validation
1550
+ import { validate, safeValidate, ValidationError } from "@lolyjs/core";
1551
+
1552
+ // Security
1553
+ import { sanitizeString, sanitizeObject } from "@lolyjs/core";
1554
+ import {
1555
+ createRateLimiter,
1556
+ defaultRateLimiter,
1557
+ strictRateLimiter,
1558
+ } from "@lolyjs/core";
1559
+
1560
+ // Logging
1561
+ import { logger, createModuleLogger, getRequestLogger } from "@lolyjs/core";
1562
+
1563
+ // Client
1564
+ import { Link } from "@lolyjs/core/components";
1565
+ import { lolySocket } from "@lolyjs/core/sockets";
1566
+ import { revalidate, revalidatePath } from "@lolyjs/core/client-cache";
1567
+ ```
1568
+
1569
+ ---
1570
+
1571
+ ## License
1572
+
1573
+ ISC
1574
+
1575
+ ---
1576
+
1577
+ ## Built With
1578
+
1579
+ - [React](https://react.dev/) - UI library
1580
+ - [Express](https://expressjs.com/) - Web framework
1581
+ - [Rspack](https://rspack.dev/) - Fast bundler
1582
+ - [Socket.IO](https://socket.io/) - WebSocket library
1583
+ - [Pino](https://getpino.io/) - Fast logger
1584
+ - [Zod](https://zod.dev/) - Schema validation
1585
+ - [Helmet](https://helmetjs.github.io/) - Security headers
1586
+
1587
+ ---
1588
+
1589
+ <div align="center">
1590
+
1591
+ **Made with ❤️ by the Loly team**
1592
+
1593
+ </div>