@lolyjs/core 0.2.0-alpha.14 → 0.2.0-alpha.15
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 +99 -12
- package/dist/cli.cjs +192 -44
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +192 -44
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +192 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +192 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Loly is a full-stack React framework that combines the simplicity of file-based
|
|
|
37
37
|
|
|
38
38
|
- 🔌 **Native WebSocket Support** - Built-in Socket.IO integration with automatic namespace routing
|
|
39
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
|
|
40
|
+
- 📁 **Separation of Concerns** - Server logic in `page.server.hook.ts` and `layout.server.hook.ts` separate from React components
|
|
41
41
|
- 🚀 **Hybrid Rendering** - SSR, SSG, and CSR with streaming support
|
|
42
42
|
- 🛡️ **Security First** - Built-in rate limiting, validation, sanitization, and security headers
|
|
43
43
|
- ⚡ **Performance** - Fast bundling with Rspack and optimized code splitting
|
|
@@ -66,7 +66,7 @@ export default function Home() {
|
|
|
66
66
|
### Add Server-Side Data
|
|
67
67
|
|
|
68
68
|
```tsx
|
|
69
|
-
// app/page/server.hook.ts
|
|
69
|
+
// app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
|
|
70
70
|
import type { ServerLoader } from "@lolyjs/core";
|
|
71
71
|
|
|
72
72
|
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
@@ -157,7 +157,7 @@ Define middlewares directly in your routes for fine-grained control:
|
|
|
157
157
|
**For Pages:**
|
|
158
158
|
|
|
159
159
|
```tsx
|
|
160
|
-
// app/dashboard/server.hook.ts
|
|
160
|
+
// app/dashboard/page.server.hook.ts (preferred) or app/dashboard/server.hook.ts (legacy)
|
|
161
161
|
import type { RouteMiddleware, ServerLoader } from "@lolyjs/core";
|
|
162
162
|
|
|
163
163
|
export const beforeServerData: RouteMiddleware[] = [
|
|
@@ -233,22 +233,37 @@ Routes are automatically created from your file structure:
|
|
|
233
233
|
|
|
234
234
|
```tsx
|
|
235
235
|
// app/layout.tsx (Root layout)
|
|
236
|
-
export default function RootLayout({ children }) {
|
|
236
|
+
export default function RootLayout({ children, appName, navigation }) {
|
|
237
237
|
return (
|
|
238
238
|
<div>
|
|
239
|
-
<nav>
|
|
239
|
+
<nav>{navigation}</nav>
|
|
240
240
|
{children}
|
|
241
|
-
<footer>
|
|
241
|
+
<footer>{appName}</footer>
|
|
242
242
|
</div>
|
|
243
243
|
);
|
|
244
244
|
}
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
+
```tsx
|
|
248
|
+
// app/layout.server.hook.ts (Root layout server hook - same directory as layout.tsx)
|
|
249
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
250
|
+
|
|
251
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
252
|
+
return {
|
|
253
|
+
props: {
|
|
254
|
+
appName: "My App",
|
|
255
|
+
navigation: ["Home", "About", "Blog"],
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
```
|
|
260
|
+
|
|
247
261
|
```tsx
|
|
248
262
|
// app/blog/layout.tsx (Nested layout)
|
|
249
|
-
export default function BlogLayout({ children }) {
|
|
263
|
+
export default function BlogLayout({ children, sectionTitle }) {
|
|
250
264
|
return (
|
|
251
265
|
<div>
|
|
266
|
+
<h1>{sectionTitle}</h1>
|
|
252
267
|
<aside>Sidebar</aside>
|
|
253
268
|
<main>{children}</main>
|
|
254
269
|
</div>
|
|
@@ -256,6 +271,31 @@ export default function BlogLayout({ children }) {
|
|
|
256
271
|
}
|
|
257
272
|
```
|
|
258
273
|
|
|
274
|
+
```tsx
|
|
275
|
+
// app/blog/layout.server.hook.ts (Nested layout server hook - same directory as layout.tsx)
|
|
276
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
277
|
+
|
|
278
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
279
|
+
return {
|
|
280
|
+
props: {
|
|
281
|
+
sectionTitle: "Blog Section",
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Layout Server Hooks:**
|
|
288
|
+
|
|
289
|
+
Layouts can have their own server hooks that provide stable data across all pages. Props from layout server hooks are automatically merged with page props:
|
|
290
|
+
|
|
291
|
+
- **Layout props** (from `layout.server.hook.ts`) are stable and available to both the layout and all pages
|
|
292
|
+
- **Page props** (from `page.server.hook.ts`) are specific to each page and override layout props if there's a conflict
|
|
293
|
+
- **Combined props** are available to both layouts and pages
|
|
294
|
+
|
|
295
|
+
**File Convention:**
|
|
296
|
+
- Layout server hooks: `app/layout.server.hook.ts` (same directory as `layout.tsx`)
|
|
297
|
+
- Page server hooks: `app/page.server.hook.ts` (preferred) or `app/server.hook.ts` (legacy, backward compatible)
|
|
298
|
+
|
|
259
299
|
### 🚀 Hybrid Rendering
|
|
260
300
|
|
|
261
301
|
Choose the best rendering strategy for each page:
|
|
@@ -263,7 +303,7 @@ Choose the best rendering strategy for each page:
|
|
|
263
303
|
**SSR (Server-Side Rendering):**
|
|
264
304
|
|
|
265
305
|
```tsx
|
|
266
|
-
// app/posts/server.hook.ts
|
|
306
|
+
// app/posts/page.server.hook.ts (preferred) or app/posts/server.hook.ts (legacy)
|
|
267
307
|
export const dynamic = "force-dynamic" as const;
|
|
268
308
|
|
|
269
309
|
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
@@ -275,7 +315,7 @@ export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
|
275
315
|
**SSG (Static Site Generation):**
|
|
276
316
|
|
|
277
317
|
```tsx
|
|
278
|
-
// app/blog/[slug]/server.hook.ts
|
|
318
|
+
// app/blog/[slug]/page.server.hook.ts (preferred) or app/blog/[slug]/server.hook.ts (legacy)
|
|
279
319
|
export const dynamic = "force-static" as const;
|
|
280
320
|
|
|
281
321
|
export const generateStaticParams: GenerateStaticParams = async () => {
|
|
@@ -292,7 +332,7 @@ export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
|
292
332
|
**CSR (Client-Side Rendering):**
|
|
293
333
|
|
|
294
334
|
```tsx
|
|
295
|
-
// app/dashboard/page.tsx (No server.hook.ts)
|
|
335
|
+
// app/dashboard/page.tsx (No page.server.hook.ts)
|
|
296
336
|
import { useState, useEffect } from "react";
|
|
297
337
|
|
|
298
338
|
export default function Dashboard() {
|
|
@@ -407,16 +447,18 @@ logger.error("Error occurred", error);
|
|
|
407
447
|
your-app/
|
|
408
448
|
├── app/
|
|
409
449
|
│ ├── layout.tsx # Root layout
|
|
450
|
+
│ ├── layout.server.hook.ts # Root layout server hook (stable props)
|
|
410
451
|
│ ├── page.tsx # Home page (/)
|
|
411
|
-
│ ├── server.hook.ts
|
|
452
|
+
│ ├── page.server.hook.ts # Page server hook (preferred) or server.hook.ts (legacy)
|
|
412
453
|
│ ├── _not-found.tsx # Custom 404
|
|
413
454
|
│ ├── _error.tsx # Custom error page
|
|
414
455
|
│ ├── blog/
|
|
415
456
|
│ │ ├── layout.tsx # Blog layout
|
|
457
|
+
│ │ ├── layout.server.hook.ts # Blog layout server hook
|
|
416
458
|
│ │ ├── page.tsx # /blog
|
|
417
459
|
│ │ └── [slug]/
|
|
418
460
|
│ │ ├── page.tsx # /blog/:slug
|
|
419
|
-
│ │ └── server.hook.ts #
|
|
461
|
+
│ │ └── page.server.hook.ts # Page server hook
|
|
420
462
|
│ ├── api/
|
|
421
463
|
│ │ └── posts/
|
|
422
464
|
│ │ └── route.ts # /api/posts
|
|
@@ -437,7 +479,10 @@ your-app/
|
|
|
437
479
|
|
|
438
480
|
### Server Loader
|
|
439
481
|
|
|
482
|
+
**Page Server Hook:**
|
|
483
|
+
|
|
440
484
|
```tsx
|
|
485
|
+
// app/page.server.hook.ts (preferred) or app/server.hook.ts (legacy)
|
|
441
486
|
import type { ServerLoader } from "@lolyjs/core";
|
|
442
487
|
|
|
443
488
|
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
@@ -468,6 +513,48 @@ export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
|
468
513
|
};
|
|
469
514
|
```
|
|
470
515
|
|
|
516
|
+
**Layout Server Hook:**
|
|
517
|
+
|
|
518
|
+
```tsx
|
|
519
|
+
// app/layout.server.hook.ts (same directory as layout.tsx)
|
|
520
|
+
import type { ServerLoader } from "@lolyjs/core";
|
|
521
|
+
|
|
522
|
+
export const getServerSideProps: ServerLoader = async (ctx) => {
|
|
523
|
+
// Fetch stable data that persists across all pages
|
|
524
|
+
const user = await getCurrentUser();
|
|
525
|
+
const navigation = await getNavigation();
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
props: {
|
|
529
|
+
user, // Available to layout and all pages
|
|
530
|
+
navigation, // Available to layout and all pages
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Props Merging:**
|
|
537
|
+
|
|
538
|
+
- Layout props (from `layout.server.hook.ts`) are merged first
|
|
539
|
+
- Page props (from `page.server.hook.ts`) are merged second and override layout props
|
|
540
|
+
- Both layouts and pages receive the combined props
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
// app/layout.tsx
|
|
544
|
+
export default function Layout({ user, navigation, children }) {
|
|
545
|
+
// Receives: user, navigation (from layout.server.hook.ts)
|
|
546
|
+
// Also receives: any props from page.server.hook.ts
|
|
547
|
+
return <div>{/* ... */}</div>;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// app/page.tsx
|
|
551
|
+
export default function Page({ user, navigation, posts }) {
|
|
552
|
+
// Receives: user, navigation (from layout.server.hook.ts)
|
|
553
|
+
// Receives: posts (from page.server.hook.ts)
|
|
554
|
+
return <div>{/* ... */}</div>;
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
471
558
|
### API Route Handler
|
|
472
559
|
|
|
473
560
|
```tsx
|
package/dist/cli.cjs
CHANGED
|
@@ -169,7 +169,7 @@ function loadLayoutsForDir(pageDir, appDir) {
|
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
// modules/router/
|
|
172
|
+
// modules/router/server-hook.ts
|
|
173
173
|
var import_fs2 = __toESM(require("fs"));
|
|
174
174
|
var import_path2 = __toESM(require("path"));
|
|
175
175
|
var NAMING = {
|
|
@@ -181,14 +181,16 @@ var NAMING = {
|
|
|
181
181
|
// Files
|
|
182
182
|
SERVER_HOOK: "server.hook"
|
|
183
183
|
};
|
|
184
|
-
function
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const
|
|
184
|
+
function loadServerHookForDir(currentDir) {
|
|
185
|
+
const pageServerHookTs = import_path2.default.join(currentDir, `page.server.hook.ts`);
|
|
186
|
+
const pageServerHookJs = import_path2.default.join(currentDir, `page.server.hook.js`);
|
|
187
|
+
const serverHookTs = import_path2.default.join(currentDir, `${NAMING.SERVER_HOOK}.ts`);
|
|
188
|
+
const serverHookJs = import_path2.default.join(currentDir, `${NAMING.SERVER_HOOK}.js`);
|
|
189
|
+
const file = import_fs2.default.existsSync(pageServerHookTs) ? pageServerHookTs : import_fs2.default.existsSync(pageServerHookJs) ? pageServerHookJs : import_fs2.default.existsSync(serverHookTs) ? serverHookTs : import_fs2.default.existsSync(serverHookJs) ? serverHookJs : null;
|
|
188
190
|
if (!file) {
|
|
189
191
|
return {
|
|
190
192
|
middlewares: [],
|
|
191
|
-
|
|
193
|
+
serverHook: null,
|
|
192
194
|
dynamic: "auto",
|
|
193
195
|
generateStaticParams: null
|
|
194
196
|
};
|
|
@@ -204,12 +206,12 @@ function loadLoaderForDir(currentDir) {
|
|
|
204
206
|
mod = require(file);
|
|
205
207
|
} catch (error) {
|
|
206
208
|
console.error(
|
|
207
|
-
`[framework][
|
|
209
|
+
`[framework][server-hook] Error loading server hook from ${file}:`,
|
|
208
210
|
error
|
|
209
211
|
);
|
|
210
212
|
return {
|
|
211
213
|
middlewares: [],
|
|
212
|
-
|
|
214
|
+
serverHook: null,
|
|
213
215
|
dynamic: "auto",
|
|
214
216
|
generateStaticParams: null
|
|
215
217
|
};
|
|
@@ -217,16 +219,43 @@ function loadLoaderForDir(currentDir) {
|
|
|
217
219
|
const middlewares = Array.isArray(
|
|
218
220
|
mod?.[NAMING.BEFORE_MIDDLEWARES]
|
|
219
221
|
) ? mod[NAMING.BEFORE_MIDDLEWARES] : [];
|
|
220
|
-
const
|
|
222
|
+
const serverHook = typeof mod?.[NAMING.GET_SERVER_DATA_FN] === "function" ? mod[NAMING.GET_SERVER_DATA_FN] : null;
|
|
221
223
|
const dynamic = mod?.[NAMING.RENDER_TYPE_CONST] === "force-static" || mod?.[NAMING.RENDER_TYPE_CONST] === "force-dynamic" ? mod.dynamic : "auto";
|
|
222
224
|
const generateStaticParams = typeof mod?.[NAMING.GENERATE_SSG_PARAMS] === "function" ? mod[NAMING.GENERATE_SSG_PARAMS] : null;
|
|
223
225
|
return {
|
|
224
226
|
middlewares,
|
|
225
|
-
|
|
227
|
+
serverHook,
|
|
226
228
|
dynamic,
|
|
227
229
|
generateStaticParams
|
|
228
230
|
};
|
|
229
231
|
}
|
|
232
|
+
function loadLayoutServerHook(layoutFile) {
|
|
233
|
+
const layoutDir = import_path2.default.dirname(layoutFile);
|
|
234
|
+
const layoutBasename = import_path2.default.basename(layoutFile, import_path2.default.extname(layoutFile));
|
|
235
|
+
const serverHookTs = import_path2.default.join(layoutDir, `${layoutBasename}.server.hook.ts`);
|
|
236
|
+
const serverHookJs = import_path2.default.join(layoutDir, `${layoutBasename}.server.hook.js`);
|
|
237
|
+
const file = import_fs2.default.existsSync(serverHookTs) ? serverHookTs : import_fs2.default.existsSync(serverHookJs) ? serverHookJs : null;
|
|
238
|
+
if (!file) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
if (file.endsWith(".ts") || file.endsWith(".tsx")) {
|
|
242
|
+
try {
|
|
243
|
+
require("tsx/cjs");
|
|
244
|
+
} catch (e) {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const mod = require(file);
|
|
249
|
+
const serverHook = typeof mod?.getServerSideProps === "function" ? mod.getServerSideProps : null;
|
|
250
|
+
return serverHook;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error(
|
|
253
|
+
`[framework][server-hook] Error loading layout server hook from ${file}:`,
|
|
254
|
+
error
|
|
255
|
+
);
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
230
259
|
|
|
231
260
|
// modules/router/loader-pages.ts
|
|
232
261
|
function loadRoutes(appDir) {
|
|
@@ -258,7 +287,12 @@ function loadRoutes(appDir) {
|
|
|
258
287
|
currentDir,
|
|
259
288
|
appDir
|
|
260
289
|
);
|
|
261
|
-
const
|
|
290
|
+
const layoutServerHooks = [];
|
|
291
|
+
for (const layoutFile of layoutFiles) {
|
|
292
|
+
const layoutServerHook = loadLayoutServerHook(layoutFile);
|
|
293
|
+
layoutServerHooks.push(layoutServerHook);
|
|
294
|
+
}
|
|
295
|
+
const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(currentDir);
|
|
262
296
|
routes.push({
|
|
263
297
|
pattern: routePath,
|
|
264
298
|
regex,
|
|
@@ -268,7 +302,10 @@ function loadRoutes(appDir) {
|
|
|
268
302
|
pageFile: fullPath,
|
|
269
303
|
layoutFiles,
|
|
270
304
|
middlewares,
|
|
271
|
-
loader,
|
|
305
|
+
loader: serverHook,
|
|
306
|
+
// Keep 'loader' field name for backward compatibility
|
|
307
|
+
layoutServerHooks,
|
|
308
|
+
// Server hooks for each layout (same order as layouts)
|
|
272
309
|
dynamic,
|
|
273
310
|
generateStaticParams
|
|
274
311
|
});
|
|
@@ -787,7 +824,12 @@ function loadRoutesFromManifest(projectRoot) {
|
|
|
787
824
|
(f) => import_path9.default.join(projectRoot, f)
|
|
788
825
|
);
|
|
789
826
|
const pageDir = import_path9.default.dirname(pageFile);
|
|
790
|
-
const
|
|
827
|
+
const layoutServerHooks = [];
|
|
828
|
+
for (const layoutFile of layoutFiles) {
|
|
829
|
+
const layoutServerHook = loadLayoutServerHook(layoutFile);
|
|
830
|
+
layoutServerHooks.push(layoutServerHook);
|
|
831
|
+
}
|
|
832
|
+
const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(pageDir);
|
|
791
833
|
pageRoutes.push({
|
|
792
834
|
pattern: entry.pattern,
|
|
793
835
|
regex,
|
|
@@ -797,7 +839,10 @@ function loadRoutesFromManifest(projectRoot) {
|
|
|
797
839
|
pageFile,
|
|
798
840
|
layoutFiles,
|
|
799
841
|
middlewares,
|
|
800
|
-
loader,
|
|
842
|
+
loader: serverHook,
|
|
843
|
+
// Keep 'loader' field name for backward compatibility
|
|
844
|
+
layoutServerHooks,
|
|
845
|
+
// Server hooks for each layout (same order as layouts)
|
|
801
846
|
dynamic: entry.dynamic ?? dynamic,
|
|
802
847
|
generateStaticParams
|
|
803
848
|
});
|
|
@@ -1051,7 +1096,12 @@ function loadNotFoundRouteFromFilesystem(appDir) {
|
|
|
1051
1096
|
notFoundDir,
|
|
1052
1097
|
appDir
|
|
1053
1098
|
);
|
|
1054
|
-
const
|
|
1099
|
+
const layoutServerHooks = [];
|
|
1100
|
+
for (const layoutFile of layoutFiles) {
|
|
1101
|
+
const layoutServerHook = loadLayoutServerHook(layoutFile);
|
|
1102
|
+
layoutServerHooks.push(layoutServerHook);
|
|
1103
|
+
}
|
|
1104
|
+
const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(notFoundDir);
|
|
1055
1105
|
return {
|
|
1056
1106
|
pattern: NOT_FOUND_PATTERN,
|
|
1057
1107
|
regex: new RegExp(`^${NOT_FOUND_PATTERN}/?$`),
|
|
@@ -1061,7 +1111,10 @@ function loadNotFoundRouteFromFilesystem(appDir) {
|
|
|
1061
1111
|
pageFile: notFoundFile,
|
|
1062
1112
|
layoutFiles,
|
|
1063
1113
|
middlewares,
|
|
1064
|
-
loader,
|
|
1114
|
+
loader: serverHook,
|
|
1115
|
+
// Keep 'loader' field name for backward compatibility
|
|
1116
|
+
layoutServerHooks,
|
|
1117
|
+
// Server hooks for each layout (same order as layouts)
|
|
1065
1118
|
dynamic,
|
|
1066
1119
|
generateStaticParams
|
|
1067
1120
|
};
|
|
@@ -1092,7 +1145,12 @@ function loadErrorRouteFromFilesystem(appDir) {
|
|
|
1092
1145
|
appDir,
|
|
1093
1146
|
appDir
|
|
1094
1147
|
);
|
|
1095
|
-
const
|
|
1148
|
+
const layoutServerHooks = [];
|
|
1149
|
+
for (const layoutFile of layoutFiles) {
|
|
1150
|
+
const layoutServerHook = loadLayoutServerHook(layoutFile);
|
|
1151
|
+
layoutServerHooks.push(layoutServerHook);
|
|
1152
|
+
}
|
|
1153
|
+
const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(appDir);
|
|
1096
1154
|
return {
|
|
1097
1155
|
pattern: ERROR_PATTERN,
|
|
1098
1156
|
regex: new RegExp(`^${ERROR_PATTERN}/?$`),
|
|
@@ -1102,7 +1160,10 @@ function loadErrorRouteFromFilesystem(appDir) {
|
|
|
1102
1160
|
pageFile: errorFile,
|
|
1103
1161
|
layoutFiles,
|
|
1104
1162
|
middlewares,
|
|
1105
|
-
loader,
|
|
1163
|
+
loader: serverHook,
|
|
1164
|
+
// Keep 'loader' field name for backward compatibility
|
|
1165
|
+
layoutServerHooks,
|
|
1166
|
+
// Server hooks for each layout (same order as layouts)
|
|
1106
1167
|
dynamic,
|
|
1107
1168
|
generateStaticParams
|
|
1108
1169
|
};
|
|
@@ -4331,8 +4392,8 @@ async function runRouteMiddlewares(route, ctx) {
|
|
|
4331
4392
|
}
|
|
4332
4393
|
}
|
|
4333
4394
|
|
|
4334
|
-
// modules/server/handlers/
|
|
4335
|
-
async function
|
|
4395
|
+
// modules/server/handlers/server-hook.ts
|
|
4396
|
+
async function runRouteServerHook(route, ctx) {
|
|
4336
4397
|
if (!route.loader) {
|
|
4337
4398
|
return { props: {} };
|
|
4338
4399
|
}
|
|
@@ -4489,11 +4550,39 @@ async function handlePageRequestInternal(options) {
|
|
|
4489
4550
|
pathname: urlPath,
|
|
4490
4551
|
locals: {}
|
|
4491
4552
|
};
|
|
4492
|
-
|
|
4553
|
+
const layoutProps2 = {};
|
|
4554
|
+
if (notFoundPage.layoutServerHooks && notFoundPage.layoutServerHooks.length > 0) {
|
|
4555
|
+
for (let i = 0; i < notFoundPage.layoutServerHooks.length; i++) {
|
|
4556
|
+
const layoutServerHook = notFoundPage.layoutServerHooks[i];
|
|
4557
|
+
if (layoutServerHook) {
|
|
4558
|
+
try {
|
|
4559
|
+
const layoutResult = await layoutServerHook(ctx2);
|
|
4560
|
+
if (layoutResult.props) {
|
|
4561
|
+
Object.assign(layoutProps2, layoutResult.props);
|
|
4562
|
+
}
|
|
4563
|
+
} catch (error) {
|
|
4564
|
+
const reqLogger2 = getRequestLogger(req);
|
|
4565
|
+
reqLogger2.warn(`Layout server hook ${i} failed for not-found`, {
|
|
4566
|
+
error,
|
|
4567
|
+
layoutFile: notFoundPage.layoutFiles[i]
|
|
4568
|
+
});
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
let loaderResult2 = await runRouteServerHook(notFoundPage, ctx2);
|
|
4493
4574
|
if (!loaderResult2.theme) {
|
|
4494
4575
|
loaderResult2.theme = theme;
|
|
4495
4576
|
}
|
|
4496
|
-
const
|
|
4577
|
+
const combinedProps2 = {
|
|
4578
|
+
...layoutProps2,
|
|
4579
|
+
...loaderResult2.props || {}
|
|
4580
|
+
};
|
|
4581
|
+
const combinedLoaderResult2 = {
|
|
4582
|
+
...loaderResult2,
|
|
4583
|
+
props: combinedProps2
|
|
4584
|
+
};
|
|
4585
|
+
const initialData2 = buildInitialData(urlPath, {}, combinedLoaderResult2);
|
|
4497
4586
|
const appTree2 = buildAppTree(notFoundPage, {}, initialData2.props);
|
|
4498
4587
|
initialData2.notFound = true;
|
|
4499
4588
|
const nonce2 = res.locals.nonce || void 0;
|
|
@@ -4501,7 +4590,7 @@ async function handlePageRequestInternal(options) {
|
|
|
4501
4590
|
appTree: appTree2,
|
|
4502
4591
|
initialData: initialData2,
|
|
4503
4592
|
routerData,
|
|
4504
|
-
meta:
|
|
4593
|
+
meta: combinedLoaderResult2.metadata ?? null,
|
|
4505
4594
|
titleFallback: "Not found",
|
|
4506
4595
|
descriptionFallback: "Loly demo",
|
|
4507
4596
|
chunkHref: null,
|
|
@@ -4520,8 +4609,8 @@ async function handlePageRequestInternal(options) {
|
|
|
4520
4609
|
},
|
|
4521
4610
|
onShellError(err) {
|
|
4522
4611
|
didError2 = true;
|
|
4523
|
-
const
|
|
4524
|
-
|
|
4612
|
+
const reqLogger2 = getRequestLogger(req);
|
|
4613
|
+
reqLogger2.error("SSR shell error", err, { route: "not-found" });
|
|
4525
4614
|
if (!res.headersSent && errorPage) {
|
|
4526
4615
|
renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
|
|
4527
4616
|
} else if (!res.headersSent) {
|
|
@@ -4533,8 +4622,8 @@ async function handlePageRequestInternal(options) {
|
|
|
4533
4622
|
},
|
|
4534
4623
|
onError(err) {
|
|
4535
4624
|
didError2 = true;
|
|
4536
|
-
const
|
|
4537
|
-
|
|
4625
|
+
const reqLogger2 = getRequestLogger(req);
|
|
4626
|
+
reqLogger2.error("SSR error", err, { route: "not-found" });
|
|
4538
4627
|
}
|
|
4539
4628
|
});
|
|
4540
4629
|
req.on("close", () => abort2());
|
|
@@ -4556,9 +4645,30 @@ async function handlePageRequestInternal(options) {
|
|
|
4556
4645
|
if (res.headersSent) {
|
|
4557
4646
|
return;
|
|
4558
4647
|
}
|
|
4648
|
+
const layoutProps = {};
|
|
4649
|
+
const reqLogger = getRequestLogger(req);
|
|
4650
|
+
if (route.layoutServerHooks && route.layoutServerHooks.length > 0) {
|
|
4651
|
+
for (let i = 0; i < route.layoutServerHooks.length; i++) {
|
|
4652
|
+
const layoutServerHook = route.layoutServerHooks[i];
|
|
4653
|
+
if (layoutServerHook) {
|
|
4654
|
+
try {
|
|
4655
|
+
const layoutResult = await layoutServerHook(ctx);
|
|
4656
|
+
if (layoutResult.props) {
|
|
4657
|
+
Object.assign(layoutProps, layoutResult.props);
|
|
4658
|
+
}
|
|
4659
|
+
} catch (error) {
|
|
4660
|
+
reqLogger.warn(`Layout server hook ${i} failed`, {
|
|
4661
|
+
error,
|
|
4662
|
+
layoutFile: route.layoutFiles[i],
|
|
4663
|
+
route: route.pattern
|
|
4664
|
+
});
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4559
4669
|
let loaderResult;
|
|
4560
4670
|
try {
|
|
4561
|
-
loaderResult = await
|
|
4671
|
+
loaderResult = await runRouteServerHook(route, ctx);
|
|
4562
4672
|
if (!loaderResult.theme) {
|
|
4563
4673
|
loaderResult.theme = theme;
|
|
4564
4674
|
}
|
|
@@ -4577,8 +4687,18 @@ async function handlePageRequestInternal(options) {
|
|
|
4577
4687
|
}
|
|
4578
4688
|
}
|
|
4579
4689
|
}
|
|
4690
|
+
const combinedProps = {
|
|
4691
|
+
...layoutProps,
|
|
4692
|
+
// Props from layouts (stable)
|
|
4693
|
+
...loaderResult.props || {}
|
|
4694
|
+
// Props from page (overrides layout)
|
|
4695
|
+
};
|
|
4696
|
+
const combinedLoaderResult = {
|
|
4697
|
+
...loaderResult,
|
|
4698
|
+
props: combinedProps
|
|
4699
|
+
};
|
|
4580
4700
|
if (isDataReq) {
|
|
4581
|
-
handleDataResponse(res,
|
|
4701
|
+
handleDataResponse(res, combinedLoaderResult, theme);
|
|
4582
4702
|
return;
|
|
4583
4703
|
}
|
|
4584
4704
|
if (loaderResult.redirect) {
|
|
@@ -4593,7 +4713,7 @@ async function handlePageRequestInternal(options) {
|
|
|
4593
4713
|
}
|
|
4594
4714
|
return;
|
|
4595
4715
|
}
|
|
4596
|
-
const initialData = buildInitialData(urlPath, params,
|
|
4716
|
+
const initialData = buildInitialData(urlPath, params, combinedLoaderResult);
|
|
4597
4717
|
const appTree = buildAppTree(route, params, initialData.props);
|
|
4598
4718
|
const chunkName = routeChunks[route.pattern];
|
|
4599
4719
|
let chunkHref = null;
|
|
@@ -4609,7 +4729,7 @@ async function handlePageRequestInternal(options) {
|
|
|
4609
4729
|
appTree,
|
|
4610
4730
|
initialData,
|
|
4611
4731
|
routerData,
|
|
4612
|
-
meta:
|
|
4732
|
+
meta: combinedLoaderResult.metadata,
|
|
4613
4733
|
titleFallback: "Loly framework",
|
|
4614
4734
|
descriptionFallback: "Loly demo",
|
|
4615
4735
|
chunkHref,
|
|
@@ -4630,8 +4750,8 @@ async function handlePageRequestInternal(options) {
|
|
|
4630
4750
|
},
|
|
4631
4751
|
onShellError(err) {
|
|
4632
4752
|
didError = true;
|
|
4633
|
-
const
|
|
4634
|
-
|
|
4753
|
+
const reqLogger2 = getRequestLogger(req);
|
|
4754
|
+
reqLogger2.error("SSR shell error", err, { route: matched?.route?.pattern || "unknown" });
|
|
4635
4755
|
if (!res.headersSent && errorPage) {
|
|
4636
4756
|
renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
|
|
4637
4757
|
} else if (!res.headersSent) {
|
|
@@ -4643,8 +4763,8 @@ async function handlePageRequestInternal(options) {
|
|
|
4643
4763
|
},
|
|
4644
4764
|
onError(err) {
|
|
4645
4765
|
didError = true;
|
|
4646
|
-
const
|
|
4647
|
-
|
|
4766
|
+
const reqLogger2 = getRequestLogger(req);
|
|
4767
|
+
reqLogger2.error("SSR error", err, { route: matched?.route?.pattern || "unknown" });
|
|
4648
4768
|
}
|
|
4649
4769
|
});
|
|
4650
4770
|
req.on("close", () => {
|
|
@@ -4661,11 +4781,39 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
|
|
|
4661
4781
|
pathname: req.path,
|
|
4662
4782
|
locals: { error }
|
|
4663
4783
|
};
|
|
4664
|
-
|
|
4784
|
+
const layoutProps = {};
|
|
4785
|
+
const reqLogger = getRequestLogger(req);
|
|
4786
|
+
if (errorPage.layoutServerHooks && errorPage.layoutServerHooks.length > 0) {
|
|
4787
|
+
for (let i = 0; i < errorPage.layoutServerHooks.length; i++) {
|
|
4788
|
+
const layoutServerHook = errorPage.layoutServerHooks[i];
|
|
4789
|
+
if (layoutServerHook) {
|
|
4790
|
+
try {
|
|
4791
|
+
const layoutResult = await layoutServerHook(ctx);
|
|
4792
|
+
if (layoutResult.props) {
|
|
4793
|
+
Object.assign(layoutProps, layoutResult.props);
|
|
4794
|
+
}
|
|
4795
|
+
} catch (err) {
|
|
4796
|
+
reqLogger.warn(`Layout server hook ${i} failed for error page`, {
|
|
4797
|
+
error: err,
|
|
4798
|
+
layoutFile: errorPage.layoutFiles[i]
|
|
4799
|
+
});
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
let loaderResult = await runRouteServerHook(errorPage, ctx);
|
|
4665
4805
|
if (!loaderResult.theme && theme) {
|
|
4666
4806
|
loaderResult.theme = theme;
|
|
4667
4807
|
}
|
|
4668
|
-
const
|
|
4808
|
+
const combinedProps = {
|
|
4809
|
+
...layoutProps,
|
|
4810
|
+
...loaderResult.props || {}
|
|
4811
|
+
};
|
|
4812
|
+
const combinedLoaderResult = {
|
|
4813
|
+
...loaderResult,
|
|
4814
|
+
props: combinedProps
|
|
4815
|
+
};
|
|
4816
|
+
const initialData = buildInitialData(req.path, { error: String(error) }, combinedLoaderResult);
|
|
4669
4817
|
const routerData = buildRouterData(req);
|
|
4670
4818
|
initialData.error = true;
|
|
4671
4819
|
if (isDataReq) {
|
|
@@ -4675,8 +4823,8 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
|
|
|
4675
4823
|
error: true,
|
|
4676
4824
|
message: String(error),
|
|
4677
4825
|
props: initialData.props,
|
|
4678
|
-
metadata:
|
|
4679
|
-
theme:
|
|
4826
|
+
metadata: combinedLoaderResult.metadata ?? null,
|
|
4827
|
+
theme: combinedLoaderResult.theme ?? theme ?? null
|
|
4680
4828
|
}));
|
|
4681
4829
|
return;
|
|
4682
4830
|
}
|
|
@@ -4698,7 +4846,7 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
|
|
|
4698
4846
|
appTree,
|
|
4699
4847
|
initialData,
|
|
4700
4848
|
routerData,
|
|
4701
|
-
meta:
|
|
4849
|
+
meta: combinedLoaderResult.metadata ?? null,
|
|
4702
4850
|
titleFallback: "Error",
|
|
4703
4851
|
descriptionFallback: "An error occurred",
|
|
4704
4852
|
chunkHref,
|
|
@@ -4719,8 +4867,8 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
|
|
|
4719
4867
|
},
|
|
4720
4868
|
onShellError(err) {
|
|
4721
4869
|
didError = true;
|
|
4722
|
-
const
|
|
4723
|
-
|
|
4870
|
+
const reqLogger2 = getRequestLogger(req);
|
|
4871
|
+
reqLogger2.error("Error rendering error page", err, { type: "shellError" });
|
|
4724
4872
|
if (!res.headersSent) {
|
|
4725
4873
|
res.statusCode = 500;
|
|
4726
4874
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
@@ -4730,8 +4878,8 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
|
|
|
4730
4878
|
},
|
|
4731
4879
|
onError(err) {
|
|
4732
4880
|
didError = true;
|
|
4733
|
-
const
|
|
4734
|
-
|
|
4881
|
+
const reqLogger2 = getRequestLogger(req);
|
|
4882
|
+
reqLogger2.error("Error in error page", err);
|
|
4735
4883
|
}
|
|
4736
4884
|
});
|
|
4737
4885
|
req.on("close", () => {
|