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