@lolyjs/core 0.1.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 +607 -0
- package/bin/loly.cjs +6 -0
- package/dist/bootstrap-BiCQmSkx.d.mts +50 -0
- package/dist/bootstrap-BiCQmSkx.d.ts +50 -0
- package/dist/cli.cjs +5186 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +5181 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +5774 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +445 -0
- package/dist/index.d.ts +445 -0
- package/dist/index.js +5731 -0
- package/dist/index.js.map +1 -0
- package/dist/react/cache.cjs +251 -0
- package/dist/react/cache.cjs.map +1 -0
- package/dist/react/cache.d.mts +59 -0
- package/dist/react/cache.d.ts +59 -0
- package/dist/react/cache.js +220 -0
- package/dist/react/cache.js.map +1 -0
- package/dist/react/components.cjs +218 -0
- package/dist/react/components.cjs.map +1 -0
- package/dist/react/components.d.mts +26 -0
- package/dist/react/components.d.ts +26 -0
- package/dist/react/components.js +190 -0
- package/dist/react/components.js.map +1 -0
- package/dist/react/hooks.cjs +86 -0
- package/dist/react/hooks.cjs.map +1 -0
- package/dist/react/hooks.d.mts +19 -0
- package/dist/react/hooks.d.ts +19 -0
- package/dist/react/hooks.js +58 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/sockets.cjs +43 -0
- package/dist/react/sockets.cjs.map +1 -0
- package/dist/react/sockets.d.mts +29 -0
- package/dist/react/sockets.d.ts +29 -0
- package/dist/react/sockets.js +18 -0
- package/dist/react/sockets.js.map +1 -0
- package/dist/react/themes.cjs +145 -0
- package/dist/react/themes.cjs.map +1 -0
- package/dist/react/themes.d.mts +13 -0
- package/dist/react/themes.d.ts +13 -0
- package/dist/react/themes.js +117 -0
- package/dist/react/themes.js.map +1 -0
- package/dist/runtime.cjs +626 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.mts +11 -0
- package/dist/runtime.d.ts +11 -0
- package/dist/runtime.js +599 -0
- package/dist/runtime.js.map +1 -0
- package/package.json +101 -0
package/README.md
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# Loly Framework
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
**A modern, production-ready React framework with file-based routing, SSR, SSG, and built-in security**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@loly/core)
|
|
8
|
+
[](https://opensource.org/licenses/ISC)
|
|
9
|
+
|
|
10
|
+
*Built with React 19, Express, Rspack, and TypeScript*
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
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 enterprise-grade security features.
|
|
19
|
+
|
|
20
|
+
### Why Loly?
|
|
21
|
+
|
|
22
|
+
- ⚡ **Fast** - Lightning-fast bundling with Rspack and optimized SSR streaming
|
|
23
|
+
- 🔒 **Secure** - Built-in rate limiting, CORS, CSP, input validation, and sanitization
|
|
24
|
+
- 🎯 **Developer-Friendly** - File-based routing, hot reload, and full TypeScript support
|
|
25
|
+
- 🚀 **Production-Ready** - Structured logging, error handling, and optimized builds
|
|
26
|
+
- 💾 **Smart Caching** - LRU cache with path indexing for optimal performance
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @loly/core react react-dom
|
|
36
|
+
# or
|
|
37
|
+
pnpm add @loly/core react react-dom
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Create Your First App
|
|
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
|
+
```
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
// app/page.tsx
|
|
52
|
+
export default function Home() {
|
|
53
|
+
return <h1>Hello, Loly!</h1>;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// bootstrap.tsx
|
|
59
|
+
import { bootstrapClient } from "@loly/core/runtime";
|
|
60
|
+
import routes from "@loly/core/runtime";
|
|
61
|
+
|
|
62
|
+
bootstrapClient(routes);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
// package.json
|
|
67
|
+
{
|
|
68
|
+
"scripts": {
|
|
69
|
+
"dev": "loly dev",
|
|
70
|
+
"build": "loly build",
|
|
71
|
+
"start": "loly start"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm run dev
|
|
78
|
+
# Server runs on http://localhost:3000
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Core Concepts
|
|
84
|
+
|
|
85
|
+
### File-Based Routing
|
|
86
|
+
|
|
87
|
+
Routes are automatically created from your file structure:
|
|
88
|
+
|
|
89
|
+
| File Path | Route |
|
|
90
|
+
|-----------|-------|
|
|
91
|
+
| `app/page.tsx` | `/` |
|
|
92
|
+
| `app/about/page.tsx` | `/about` |
|
|
93
|
+
| `app/blog/[slug]/page.tsx` | `/blog/:slug` |
|
|
94
|
+
| `app/post/[...path]/page.tsx` | `/post/*` (catch-all) |
|
|
95
|
+
|
|
96
|
+
### Server-Side Data Fetching
|
|
97
|
+
|
|
98
|
+
Use `server.hook.ts` to fetch data on the server:
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
// app/blog/[slug]/server.hook.ts
|
|
102
|
+
import type { ServerLoader } from "@loly/core";
|
|
103
|
+
|
|
104
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
105
|
+
const { slug } = ctx.params;
|
|
106
|
+
const post = await fetchPost(slug);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
props: { post },
|
|
110
|
+
metadata: {
|
|
111
|
+
title: post.title,
|
|
112
|
+
description: post.excerpt,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
// app/blog/[slug]/page.tsx
|
|
120
|
+
import { usePageProps } from "@loly/core/hooks";
|
|
121
|
+
|
|
122
|
+
export default function BlogPost() {
|
|
123
|
+
const { props } = usePageProps();
|
|
124
|
+
const { post } = props;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<article>
|
|
128
|
+
<h1>{post.title}</h1>
|
|
129
|
+
<div>{post.content}</div>
|
|
130
|
+
</article>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Client-Side Navigation
|
|
136
|
+
|
|
137
|
+
Fast page transitions without full reloads:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { Link } from "@loly/core/components";
|
|
141
|
+
|
|
142
|
+
export default function Navigation() {
|
|
143
|
+
return (
|
|
144
|
+
<nav>
|
|
145
|
+
<Link href="/">Home</Link>
|
|
146
|
+
<Link href="/about">About</Link>
|
|
147
|
+
</nav>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Cache Revalidation
|
|
153
|
+
|
|
154
|
+
Invalidate and refresh route data:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { revalidatePath, revalidate } from "@loly/core/client-cache";
|
|
158
|
+
|
|
159
|
+
// Revalidate a specific route
|
|
160
|
+
revalidatePath('/posts');
|
|
161
|
+
|
|
162
|
+
// Revalidate and refresh current page (similar to Next.js router.refresh())
|
|
163
|
+
await revalidate();
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Components using `usePageProps()` automatically update when you call `revalidate()`.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Features
|
|
171
|
+
|
|
172
|
+
### Core Features
|
|
173
|
+
|
|
174
|
+
- ✅ **File-based Routing** - Automatic route generation
|
|
175
|
+
- ✅ **Server-Side Rendering (SSR)** - React 19 streaming
|
|
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
|
|
181
|
+
|
|
182
|
+
### Security Features
|
|
183
|
+
|
|
184
|
+
- 🔒 **Rate Limiting** - Configurable with strict patterns
|
|
185
|
+
- 🔒 **CORS Protection** - Secure cross-origin sharing
|
|
186
|
+
- 🔒 **Content Security Policy** - XSS protection with nonce
|
|
187
|
+
- 🔒 **Input Validation** - Zod-based validation
|
|
188
|
+
- 🔒 **Input Sanitization** - Automatic XSS protection
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## API Reference
|
|
193
|
+
|
|
194
|
+
### Server Loader
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import type { ServerLoader } from "@loly/core";
|
|
198
|
+
|
|
199
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
200
|
+
const { req, res, params, pathname, locals } = ctx;
|
|
201
|
+
|
|
202
|
+
// Fetch data
|
|
203
|
+
const data = await fetchData();
|
|
204
|
+
|
|
205
|
+
// Redirect
|
|
206
|
+
return {
|
|
207
|
+
redirect: {
|
|
208
|
+
destination: "/new-path",
|
|
209
|
+
permanent: true,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Not found
|
|
214
|
+
return { notFound: true };
|
|
215
|
+
|
|
216
|
+
// Return props
|
|
217
|
+
return {
|
|
218
|
+
props: { data },
|
|
219
|
+
metadata: {
|
|
220
|
+
title: "Page Title",
|
|
221
|
+
description: "Page description",
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Static Site Generation
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import type { ServerLoader, GenerateStaticParams } from "@loly/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
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
import type { ApiContext } from "@loly/core";
|
|
250
|
+
import { validate } from "@loly/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
|
+
|
|
258
|
+
export async function GET(ctx: ApiContext) {
|
|
259
|
+
const posts = await fetchPosts();
|
|
260
|
+
return ctx.Response({ posts });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function POST(ctx: ApiContext) {
|
|
264
|
+
const body = validate(schema, ctx.req.body);
|
|
265
|
+
const post = await createPost(body);
|
|
266
|
+
return ctx.Response(post, 201);
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Middleware
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import type { RouteMiddleware } from "@loly/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
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Cache Management
|
|
299
|
+
|
|
300
|
+
```tsx
|
|
301
|
+
import { revalidatePath, revalidate } from "@loly/core/client-cache";
|
|
302
|
+
|
|
303
|
+
// Revalidate a specific route (removes from cache)
|
|
304
|
+
revalidatePath('/posts');
|
|
305
|
+
|
|
306
|
+
// Revalidate with query params
|
|
307
|
+
revalidatePath('/posts?page=2');
|
|
308
|
+
|
|
309
|
+
// Revalidate current page and refresh components
|
|
310
|
+
await revalidate();
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Client Hooks
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
import { usePageProps } from "@loly/core/hooks";
|
|
317
|
+
|
|
318
|
+
export default function Page() {
|
|
319
|
+
const { params, props } = usePageProps();
|
|
320
|
+
// Automatically updates when revalidate() is called
|
|
321
|
+
return <div>{/* Your UI */}</div>;
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Configuration
|
|
328
|
+
|
|
329
|
+
Create `loly.config.ts` in your project root:
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
export const config = (env: string) => {
|
|
333
|
+
return {
|
|
334
|
+
// Server
|
|
335
|
+
server: {
|
|
336
|
+
port: 3000,
|
|
337
|
+
host: env === "production" ? "0.0.0.0" : "localhost",
|
|
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
|
|
352
|
+
rateLimit: {
|
|
353
|
+
windowMs: 15 * 60 * 1000,
|
|
354
|
+
max: 100,
|
|
355
|
+
strictMax: 5,
|
|
356
|
+
strictPatterns: ["/api/auth/**"],
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
// CORS
|
|
360
|
+
corsOrigin: env === "production"
|
|
361
|
+
? ["https://yourdomain.com"]
|
|
362
|
+
: true,
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Project Structure
|
|
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
|
+
```
|
|
412
|
+
|
|
413
|
+
```tsx
|
|
414
|
+
// app/blog/layout.tsx (Nested)
|
|
415
|
+
export default function BlogLayout({ children }) {
|
|
416
|
+
return (
|
|
417
|
+
<div className="blog">
|
|
418
|
+
<aside>Sidebar</aside>
|
|
419
|
+
<main>{children}</main>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Themes
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
import { ThemeProvider } from "@loly/core/themes";
|
|
429
|
+
|
|
430
|
+
export default function RootLayout({ children, theme }) {
|
|
431
|
+
return (
|
|
432
|
+
<ThemeProvider initialTheme={theme}>
|
|
433
|
+
{children}
|
|
434
|
+
</ThemeProvider>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Validation & Sanitization
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
import { validate, safeValidate, sanitizeString } from "@loly/core";
|
|
443
|
+
import { z } from "zod";
|
|
444
|
+
|
|
445
|
+
const schema = z.object({
|
|
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
|
+
```
|
|
462
|
+
|
|
463
|
+
### Logging
|
|
464
|
+
|
|
465
|
+
```tsx
|
|
466
|
+
import { getRequestLogger, createModuleLogger } from "@loly/core";
|
|
467
|
+
|
|
468
|
+
// In server hooks or API routes
|
|
469
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
470
|
+
const logger = getRequestLogger(ctx.req);
|
|
471
|
+
logger.info("Processing request", { userId: ctx.locals.user?.id });
|
|
472
|
+
return { props: {} };
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Module logger
|
|
476
|
+
const logger = createModuleLogger("my-module");
|
|
477
|
+
logger.debug("Debug message");
|
|
478
|
+
logger.error("Error", error);
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
---
|
|
482
|
+
|
|
483
|
+
## TypeScript Support
|
|
484
|
+
|
|
485
|
+
Loly is built with TypeScript and provides full type safety:
|
|
486
|
+
|
|
487
|
+
```tsx
|
|
488
|
+
import type {
|
|
489
|
+
ServerContext,
|
|
490
|
+
ServerLoader,
|
|
491
|
+
ApiContext,
|
|
492
|
+
RouteMiddleware,
|
|
493
|
+
ApiMiddleware,
|
|
494
|
+
} from "@loly/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
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## Production
|
|
507
|
+
|
|
508
|
+
### Build
|
|
509
|
+
|
|
510
|
+
```bash
|
|
511
|
+
npm run build
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
This generates:
|
|
515
|
+
- Client bundle (`.loly/client`)
|
|
516
|
+
- Static pages if using SSG (`.loly/ssg`)
|
|
517
|
+
- Server code
|
|
518
|
+
|
|
519
|
+
### Start
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
npm run start
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Environment Variables
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
PORT=3000
|
|
529
|
+
HOST=0.0.0.0
|
|
530
|
+
LOG_LEVEL=info
|
|
531
|
+
LOG_REQUESTS=false
|
|
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]
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
## All Exports
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
// Server
|
|
555
|
+
import { startDevServer, startProdServer, buildApp } from "@loly/core";
|
|
556
|
+
|
|
557
|
+
// Types
|
|
558
|
+
import type {
|
|
559
|
+
ServerContext,
|
|
560
|
+
ServerLoader,
|
|
561
|
+
ApiContext,
|
|
562
|
+
RouteMiddleware,
|
|
563
|
+
ApiMiddleware,
|
|
564
|
+
GenerateStaticParams,
|
|
565
|
+
} from "@loly/core";
|
|
566
|
+
|
|
567
|
+
// Validation
|
|
568
|
+
import { validate, safeValidate, ValidationError } from "@loly/core";
|
|
569
|
+
|
|
570
|
+
// Security
|
|
571
|
+
import { sanitizeString, sanitizeObject } from "@loly/core";
|
|
572
|
+
import { strictRateLimiter, lenientRateLimiter } from "@loly/core";
|
|
573
|
+
|
|
574
|
+
// Logging
|
|
575
|
+
import { logger, createModuleLogger, getRequestLogger } from "@loly/core";
|
|
576
|
+
|
|
577
|
+
// Client
|
|
578
|
+
import { Link } from "@loly/core/components";
|
|
579
|
+
import { usePageProps } from "@loly/core/hooks";
|
|
580
|
+
import { ThemeProvider } from "@loly/core/themes";
|
|
581
|
+
import { revalidatePath, revalidate } from "@loly/core/client-cache";
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## License
|
|
587
|
+
|
|
588
|
+
ISC
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## Built With
|
|
593
|
+
|
|
594
|
+
- [React](https://react.dev/) - UI library
|
|
595
|
+
- [Express](https://expressjs.com/) - Web framework
|
|
596
|
+
- [Rspack](https://rspack.dev/) - Fast bundler
|
|
597
|
+
- [Pino](https://getpino.io/) - Fast logger
|
|
598
|
+
- [Zod](https://zod.dev/) - Schema validation
|
|
599
|
+
- [Helmet](https://helmetjs.github.io/) - Security headers
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
<div align="center">
|
|
604
|
+
|
|
605
|
+
**Made with ❤️ by the Loly team**
|
|
606
|
+
|
|
607
|
+
</div>
|
package/bin/loly.cjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
declare const WINDOW_DATA_KEY = "__FW_DATA__";
|
|
2
|
+
|
|
3
|
+
type InitialData = {
|
|
4
|
+
pathname: string;
|
|
5
|
+
params: Record<string, string>;
|
|
6
|
+
props: Record<string, any>;
|
|
7
|
+
metadata?: {
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
} | null;
|
|
11
|
+
notFound?: boolean;
|
|
12
|
+
error?: boolean;
|
|
13
|
+
theme?: string;
|
|
14
|
+
};
|
|
15
|
+
declare global {
|
|
16
|
+
interface Window {
|
|
17
|
+
[WINDOW_DATA_KEY]?: InitialData;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
type ClientLoadedComponents = {
|
|
21
|
+
Page: React.ComponentType<any>;
|
|
22
|
+
layouts: React.ComponentType<any>[];
|
|
23
|
+
};
|
|
24
|
+
type ClientRouteLoaded = {
|
|
25
|
+
pattern: string;
|
|
26
|
+
paramNames: string[];
|
|
27
|
+
load: () => Promise<ClientLoadedComponents>;
|
|
28
|
+
};
|
|
29
|
+
type ClientRouteMatch = {
|
|
30
|
+
route: ClientRouteLoaded;
|
|
31
|
+
params: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
type RouteViewState = {
|
|
34
|
+
url: string;
|
|
35
|
+
route: ClientRouteLoaded | null;
|
|
36
|
+
params: Record<string, string>;
|
|
37
|
+
components: ClientLoadedComponents | null;
|
|
38
|
+
props: Record<string, any>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bootstraps the client-side application.
|
|
43
|
+
*
|
|
44
|
+
* @param routes - Array of client routes
|
|
45
|
+
* @param notFoundRoute - Not-found route definition
|
|
46
|
+
* @param errorRoute - Error route definition
|
|
47
|
+
*/
|
|
48
|
+
declare function bootstrapClient(routes: ClientRouteLoaded[], notFoundRoute: ClientRouteLoaded | null, errorRoute?: ClientRouteLoaded | null): void;
|
|
49
|
+
|
|
50
|
+
export { type ClientRouteLoaded as C, type InitialData as I, type RouteViewState as R, type ClientLoadedComponents as a, type ClientRouteMatch as b, bootstrapClient as c };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
declare const WINDOW_DATA_KEY = "__FW_DATA__";
|
|
2
|
+
|
|
3
|
+
type InitialData = {
|
|
4
|
+
pathname: string;
|
|
5
|
+
params: Record<string, string>;
|
|
6
|
+
props: Record<string, any>;
|
|
7
|
+
metadata?: {
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
} | null;
|
|
11
|
+
notFound?: boolean;
|
|
12
|
+
error?: boolean;
|
|
13
|
+
theme?: string;
|
|
14
|
+
};
|
|
15
|
+
declare global {
|
|
16
|
+
interface Window {
|
|
17
|
+
[WINDOW_DATA_KEY]?: InitialData;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
type ClientLoadedComponents = {
|
|
21
|
+
Page: React.ComponentType<any>;
|
|
22
|
+
layouts: React.ComponentType<any>[];
|
|
23
|
+
};
|
|
24
|
+
type ClientRouteLoaded = {
|
|
25
|
+
pattern: string;
|
|
26
|
+
paramNames: string[];
|
|
27
|
+
load: () => Promise<ClientLoadedComponents>;
|
|
28
|
+
};
|
|
29
|
+
type ClientRouteMatch = {
|
|
30
|
+
route: ClientRouteLoaded;
|
|
31
|
+
params: Record<string, string>;
|
|
32
|
+
};
|
|
33
|
+
type RouteViewState = {
|
|
34
|
+
url: string;
|
|
35
|
+
route: ClientRouteLoaded | null;
|
|
36
|
+
params: Record<string, string>;
|
|
37
|
+
components: ClientLoadedComponents | null;
|
|
38
|
+
props: Record<string, any>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bootstraps the client-side application.
|
|
43
|
+
*
|
|
44
|
+
* @param routes - Array of client routes
|
|
45
|
+
* @param notFoundRoute - Not-found route definition
|
|
46
|
+
* @param errorRoute - Error route definition
|
|
47
|
+
*/
|
|
48
|
+
declare function bootstrapClient(routes: ClientRouteLoaded[], notFoundRoute: ClientRouteLoaded | null, errorRoute?: ClientRouteLoaded | null): void;
|
|
49
|
+
|
|
50
|
+
export { type ClientRouteLoaded as C, type InitialData as I, type RouteViewState as R, type ClientLoadedComponents as a, type ClientRouteMatch as b, bootstrapClient as c };
|