@lolyjs/core 0.2.0-alpha.14 → 0.2.0-alpha.16

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/dist/index.js CHANGED
@@ -74,7 +74,7 @@ var init_globals = __esm({
74
74
 
75
75
  // src/server.ts
76
76
  import fs16 from "fs";
77
- import path20 from "path";
77
+ import path23 from "path";
78
78
 
79
79
  // modules/server/utils/server-dir.ts
80
80
  init_globals();
@@ -204,7 +204,7 @@ function loadLayoutsForDir(pageDir, appDir) {
204
204
  };
205
205
  }
206
206
 
207
- // modules/router/loader.ts
207
+ // modules/router/server-hook.ts
208
208
  import fs3 from "fs";
209
209
  import path3 from "path";
210
210
  var NAMING = {
@@ -216,14 +216,16 @@ var NAMING = {
216
216
  // Files
217
217
  SERVER_HOOK: "server.hook"
218
218
  };
219
- function loadLoaderForDir(currentDir) {
220
- const loaderTs = path3.join(currentDir, `${NAMING.SERVER_HOOK}.ts`);
221
- const loaderJs = path3.join(currentDir, `${NAMING.SERVER_HOOK}.js`);
222
- const file = fs3.existsSync(loaderTs) ? loaderTs : fs3.existsSync(loaderJs) ? loaderJs : null;
219
+ function loadServerHookForDir(currentDir) {
220
+ const pageServerHookTs = path3.join(currentDir, `page.server.hook.ts`);
221
+ const pageServerHookJs = path3.join(currentDir, `page.server.hook.js`);
222
+ const serverHookTs = path3.join(currentDir, `${NAMING.SERVER_HOOK}.ts`);
223
+ const serverHookJs = path3.join(currentDir, `${NAMING.SERVER_HOOK}.js`);
224
+ const file = fs3.existsSync(pageServerHookTs) ? pageServerHookTs : fs3.existsSync(pageServerHookJs) ? pageServerHookJs : fs3.existsSync(serverHookTs) ? serverHookTs : fs3.existsSync(serverHookJs) ? serverHookJs : null;
223
225
  if (!file) {
224
226
  return {
225
227
  middlewares: [],
226
- loader: null,
228
+ serverHook: null,
227
229
  dynamic: "auto",
228
230
  generateStaticParams: null
229
231
  };
@@ -239,31 +241,119 @@ function loadLoaderForDir(currentDir) {
239
241
  mod = __require(file);
240
242
  } catch (error) {
241
243
  console.error(
242
- `[framework][loader] Error loading server hook from ${file}:`,
244
+ `[framework][server-hook] Error loading server hook from ${file}:`,
243
245
  error
244
246
  );
245
247
  return {
246
248
  middlewares: [],
247
- loader: null,
249
+ serverHook: null,
248
250
  dynamic: "auto",
249
251
  generateStaticParams: null
250
252
  };
251
253
  }
252
- const middlewares = Array.isArray(
253
- mod?.[NAMING.BEFORE_MIDDLEWARES]
254
- ) ? mod[NAMING.BEFORE_MIDDLEWARES] : [];
255
- const loader = typeof mod?.[NAMING.GET_SERVER_DATA_FN] === "function" ? mod[NAMING.GET_SERVER_DATA_FN] : null;
254
+ let middlewares = [];
255
+ const rawMiddlewares = mod?.[NAMING.BEFORE_MIDDLEWARES];
256
+ if (rawMiddlewares !== void 0) {
257
+ if (!Array.isArray(rawMiddlewares)) {
258
+ console.warn(
259
+ `[framework][server-hook] ${NAMING.BEFORE_MIDDLEWARES} must be an array in ${file}, ignoring invalid value`
260
+ );
261
+ } else {
262
+ for (let i = 0; i < rawMiddlewares.length; i++) {
263
+ const mw = rawMiddlewares[i];
264
+ if (typeof mw !== "function") {
265
+ console.warn(
266
+ `[framework][server-hook] Middleware at index ${i} in ${NAMING.BEFORE_MIDDLEWARES} is not a function in ${file}, skipping`
267
+ );
268
+ continue;
269
+ }
270
+ middlewares.push(mw);
271
+ }
272
+ }
273
+ }
274
+ const serverHook = typeof mod?.[NAMING.GET_SERVER_DATA_FN] === "function" ? mod[NAMING.GET_SERVER_DATA_FN] : null;
256
275
  const dynamic = mod?.[NAMING.RENDER_TYPE_CONST] === "force-static" || mod?.[NAMING.RENDER_TYPE_CONST] === "force-dynamic" ? mod.dynamic : "auto";
257
276
  const generateStaticParams = typeof mod?.[NAMING.GENERATE_SSG_PARAMS] === "function" ? mod[NAMING.GENERATE_SSG_PARAMS] : null;
258
277
  return {
259
278
  middlewares,
260
- loader,
279
+ serverHook,
261
280
  dynamic,
262
281
  generateStaticParams
263
282
  };
264
283
  }
284
+ function loadLayoutServerHook(layoutFile) {
285
+ const layoutDir = path3.dirname(layoutFile);
286
+ const layoutBasename = path3.basename(layoutFile, path3.extname(layoutFile));
287
+ const serverHookTs = path3.join(layoutDir, `${layoutBasename}.server.hook.ts`);
288
+ const serverHookJs = path3.join(layoutDir, `${layoutBasename}.server.hook.js`);
289
+ const file = fs3.existsSync(serverHookTs) ? serverHookTs : fs3.existsSync(serverHookJs) ? serverHookJs : null;
290
+ if (!file) {
291
+ return null;
292
+ }
293
+ if (file.endsWith(".ts") || file.endsWith(".tsx")) {
294
+ try {
295
+ __require("tsx/cjs");
296
+ } catch (e) {
297
+ }
298
+ }
299
+ try {
300
+ const mod = __require(file);
301
+ const serverHook = typeof mod?.getServerSideProps === "function" ? mod.getServerSideProps : null;
302
+ return serverHook;
303
+ } catch (error) {
304
+ console.error(
305
+ `[framework][server-hook] Error loading layout server hook from ${file}:`,
306
+ error
307
+ );
308
+ return null;
309
+ }
310
+ }
265
311
 
266
312
  // modules/router/loader-pages.ts
313
+ function validateRoutes(routes, appDir) {
314
+ const routePatterns = /* @__PURE__ */ new Map();
315
+ const errors = [];
316
+ const warnings = [];
317
+ for (const route of routes) {
318
+ const existing = routePatterns.get(route.pattern) || [];
319
+ existing.push(route);
320
+ routePatterns.set(route.pattern, existing);
321
+ }
322
+ for (const [pattern, duplicateRoutes] of routePatterns.entries()) {
323
+ if (duplicateRoutes.length > 1) {
324
+ const files = duplicateRoutes.map(
325
+ (r) => r.pageFile ? path4.relative(appDir, r.pageFile) : "unknown"
326
+ ).join(", ");
327
+ errors.push(
328
+ `Duplicate route pattern "${pattern}" found in multiple files:
329
+ ${files}
330
+ \u{1F4A1} Suggestion: Ensure each route has a unique path pattern`
331
+ );
332
+ }
333
+ }
334
+ for (const route of routes) {
335
+ if (!route.pageFile || !fs4.existsSync(route.pageFile)) {
336
+ warnings.push(
337
+ `Route pattern "${route.pattern}" references a missing page file`
338
+ );
339
+ }
340
+ }
341
+ if (errors.length > 0) {
342
+ const errorMessage = [
343
+ "\u274C Route validation failed:",
344
+ "",
345
+ ...errors,
346
+ "",
347
+ "\u{1F4A1} Please fix the errors above before starting the server."
348
+ ].join("\n");
349
+ throw new Error(errorMessage);
350
+ }
351
+ if (warnings.length > 0 && process.env.NODE_ENV === "development") {
352
+ console.warn("\n\u26A0\uFE0F Route warnings:");
353
+ warnings.forEach((warning) => console.warn(` \u2022 ${warning}`));
354
+ console.warn("");
355
+ }
356
+ }
267
357
  function loadRoutes(appDir) {
268
358
  if (!fs4.existsSync(appDir)) {
269
359
  return [];
@@ -293,7 +383,12 @@ function loadRoutes(appDir) {
293
383
  currentDir,
294
384
  appDir
295
385
  );
296
- const { middlewares, loader, dynamic, generateStaticParams } = loadLoaderForDir(currentDir);
386
+ const layoutServerHooks = [];
387
+ for (const layoutFile of layoutFiles) {
388
+ const layoutServerHook = loadLayoutServerHook(layoutFile);
389
+ layoutServerHooks.push(layoutServerHook);
390
+ }
391
+ const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(currentDir);
297
392
  routes.push({
298
393
  pattern: routePath,
299
394
  regex,
@@ -303,13 +398,17 @@ function loadRoutes(appDir) {
303
398
  pageFile: fullPath,
304
399
  layoutFiles,
305
400
  middlewares,
306
- loader,
401
+ loader: serverHook,
402
+ // Keep 'loader' field name for backward compatibility
403
+ layoutServerHooks,
404
+ // Server hooks for each layout (same order as layouts)
307
405
  dynamic,
308
406
  generateStaticParams
309
407
  });
310
408
  }
311
409
  }
312
410
  walk(appDir);
411
+ validateRoutes(routes, appDir);
313
412
  return routes;
314
413
  }
315
414
 
@@ -673,9 +772,12 @@ function writeClientBoostrapManifest(projectRoot) {
673
772
  lines.push("");
674
773
  lines.push(`import { bootstrapClient } from "@lolyjs/core/runtime"`);
675
774
  lines.push("");
676
- lines.push(
677
- "bootstrapClient(routes as ClientRouteLoaded[], notFoundRoute, errorRoute);"
678
- );
775
+ lines.push(`try {`);
776
+ lines.push(` bootstrapClient(routes as ClientRouteLoaded[], notFoundRoute, errorRoute);`);
777
+ lines.push(`} catch (error) {`);
778
+ lines.push(` console.error("[bootstrap] Fatal error during bootstrap:", error);`);
779
+ lines.push(` throw error;`);
780
+ lines.push(`}`);
679
781
  fs7.writeFileSync(manifestPath, lines.join("\n"), "utf-8");
680
782
  }
681
783
  function writeRoutesManifest({
@@ -822,7 +924,12 @@ function loadRoutesFromManifest(projectRoot) {
822
924
  (f) => path8.join(projectRoot, f)
823
925
  );
824
926
  const pageDir = path8.dirname(pageFile);
825
- const { middlewares, loader, dynamic, generateStaticParams } = loadLoaderForDir(pageDir);
927
+ const layoutServerHooks = [];
928
+ for (const layoutFile of layoutFiles) {
929
+ const layoutServerHook = loadLayoutServerHook(layoutFile);
930
+ layoutServerHooks.push(layoutServerHook);
931
+ }
932
+ const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(pageDir);
826
933
  pageRoutes.push({
827
934
  pattern: entry.pattern,
828
935
  regex,
@@ -832,7 +939,10 @@ function loadRoutesFromManifest(projectRoot) {
832
939
  pageFile,
833
940
  layoutFiles,
834
941
  middlewares,
835
- loader,
942
+ loader: serverHook,
943
+ // Keep 'loader' field name for backward compatibility
944
+ layoutServerHooks,
945
+ // Server hooks for each layout (same order as layouts)
836
946
  dynamic: entry.dynamic ?? dynamic,
837
947
  generateStaticParams
838
948
  });
@@ -1005,24 +1115,210 @@ function loadWssRoutes(appDir) {
1005
1115
  }
1006
1116
 
1007
1117
  // modules/router/route-loader.ts
1118
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1119
+ "node_modules",
1120
+ ".git",
1121
+ ".loly",
1122
+ "dist",
1123
+ "build",
1124
+ ".next",
1125
+ ".cache",
1126
+ "coverage",
1127
+ ".vscode",
1128
+ ".idea",
1129
+ ".turbo",
1130
+ ".swc"
1131
+ ]);
1132
+ function getRelevantFiles(appDir, projectRoot) {
1133
+ const files = [];
1134
+ const projectRootNormalized = path10.resolve(projectRoot);
1135
+ function walk(currentDir) {
1136
+ if (!fs10.existsSync(currentDir)) {
1137
+ return;
1138
+ }
1139
+ const entries = fs10.readdirSync(currentDir, { withFileTypes: true });
1140
+ for (const entry of entries) {
1141
+ const fullPath = path10.join(currentDir, entry.name);
1142
+ if (entry.isDirectory()) {
1143
+ if (SKIP_DIRECTORIES.has(entry.name)) {
1144
+ continue;
1145
+ }
1146
+ if (entry.name.startsWith(".")) {
1147
+ continue;
1148
+ }
1149
+ walk(fullPath);
1150
+ } else {
1151
+ const ext = path10.extname(entry.name);
1152
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
1153
+ files.push(fullPath);
1154
+ }
1155
+ }
1156
+ }
1157
+ }
1158
+ walk(projectRootNormalized);
1159
+ return files;
1160
+ }
1161
+ function hasFilesChanged(appDir, projectRoot, cachedStats) {
1162
+ const currentFiles = getRelevantFiles(appDir, projectRoot);
1163
+ const currentFilesSet = new Set(currentFiles);
1164
+ for (const [filePath] of cachedStats.entries()) {
1165
+ if (!currentFilesSet.has(filePath)) {
1166
+ return true;
1167
+ }
1168
+ }
1169
+ for (const filePath of currentFiles) {
1170
+ if (!fs10.existsSync(filePath)) {
1171
+ continue;
1172
+ }
1173
+ const stats = fs10.statSync(filePath);
1174
+ const cachedStat = cachedStats.get(filePath);
1175
+ if (!cachedStat) {
1176
+ return true;
1177
+ }
1178
+ if (stats.mtimeMs !== cachedStat.mtime || stats.size !== cachedStat.size) {
1179
+ return true;
1180
+ }
1181
+ }
1182
+ return false;
1183
+ }
1184
+ function buildFileStats(files) {
1185
+ const statsMap = /* @__PURE__ */ new Map();
1186
+ for (const filePath of files) {
1187
+ if (fs10.existsSync(filePath)) {
1188
+ const stats = fs10.statSync(filePath);
1189
+ statsMap.set(filePath, {
1190
+ mtime: stats.mtimeMs,
1191
+ size: stats.size
1192
+ });
1193
+ }
1194
+ }
1195
+ return statsMap;
1196
+ }
1008
1197
  var FilesystemRouteLoader = class {
1009
- constructor(appDir) {
1198
+ // Maximum cache age in ms (1 second fallback)
1199
+ constructor(appDir, projectRoot = appDir) {
1010
1200
  this.appDir = appDir;
1201
+ this.projectRoot = projectRoot;
1202
+ this.cache = null;
1203
+ this.cacheMaxAge = 1e3;
1204
+ if (this.projectRoot === this.appDir) {
1205
+ let current = path10.resolve(this.appDir);
1206
+ while (current !== path10.dirname(current)) {
1207
+ if (fs10.existsSync(path10.join(current, "package.json"))) {
1208
+ this.projectRoot = current;
1209
+ break;
1210
+ }
1211
+ current = path10.dirname(current);
1212
+ }
1213
+ }
1214
+ }
1215
+ /**
1216
+ * Invalidates the cache, forcing a reload on next access.
1217
+ */
1218
+ invalidateCache() {
1219
+ this.cache = null;
1220
+ }
1221
+ /**
1222
+ * Checks if cache is still valid, invalidates if files changed.
1223
+ */
1224
+ ensureCacheValid() {
1225
+ if (!this.cache) {
1226
+ return;
1227
+ }
1228
+ const now = Date.now();
1229
+ if (now - this.cache.timestamp > this.cacheMaxAge) {
1230
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats)) {
1231
+ this.cache = null;
1232
+ } else {
1233
+ this.cache.timestamp = now;
1234
+ }
1235
+ }
1011
1236
  }
1012
1237
  loadRoutes() {
1013
- return loadRoutes(this.appDir);
1238
+ this.ensureCacheValid();
1239
+ if (!this.cache || hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats)) {
1240
+ const routes = loadRoutes(this.appDir);
1241
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1242
+ const fileStats = buildFileStats(files);
1243
+ this.cache = {
1244
+ routes,
1245
+ apiRoutes: this.cache?.apiRoutes || [],
1246
+ wssRoutes: this.cache?.wssRoutes || [],
1247
+ notFoundRoute: this.cache?.notFoundRoute ?? null,
1248
+ errorRoute: this.cache?.errorRoute ?? null,
1249
+ fileStats,
1250
+ timestamp: Date.now()
1251
+ };
1252
+ }
1253
+ return this.cache.routes;
1014
1254
  }
1015
1255
  loadApiRoutes() {
1016
- return loadApiRoutes(this.appDir);
1256
+ this.ensureCacheValid();
1257
+ if (!this.cache) {
1258
+ this.loadRoutes();
1259
+ }
1260
+ if (!this.cache) {
1261
+ throw new Error("Failed to initialize route cache");
1262
+ }
1263
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.apiRoutes.length === 0) {
1264
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1265
+ const fileStats = buildFileStats(files);
1266
+ this.cache.apiRoutes = loadApiRoutes(this.appDir);
1267
+ this.cache.fileStats = fileStats;
1268
+ this.cache.timestamp = Date.now();
1269
+ }
1270
+ return this.cache.apiRoutes;
1017
1271
  }
1018
1272
  loadWssRoutes() {
1019
- return loadWssRoutes(this.appDir);
1273
+ this.ensureCacheValid();
1274
+ if (!this.cache) {
1275
+ this.loadRoutes();
1276
+ }
1277
+ if (!this.cache) {
1278
+ throw new Error("Failed to initialize route cache");
1279
+ }
1280
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.wssRoutes.length === 0) {
1281
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1282
+ const fileStats = buildFileStats(files);
1283
+ this.cache.wssRoutes = loadWssRoutes(this.appDir);
1284
+ this.cache.fileStats = fileStats;
1285
+ this.cache.timestamp = Date.now();
1286
+ }
1287
+ return this.cache.wssRoutes;
1020
1288
  }
1021
1289
  loadNotFoundRoute() {
1022
- return loadNotFoundRouteFromFilesystem(this.appDir);
1290
+ this.ensureCacheValid();
1291
+ if (!this.cache) {
1292
+ this.loadRoutes();
1293
+ }
1294
+ if (!this.cache) {
1295
+ throw new Error("Failed to initialize route cache");
1296
+ }
1297
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.notFoundRoute === void 0) {
1298
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1299
+ const fileStats = buildFileStats(files);
1300
+ this.cache.notFoundRoute = loadNotFoundRouteFromFilesystem(this.appDir);
1301
+ this.cache.fileStats = fileStats;
1302
+ this.cache.timestamp = Date.now();
1303
+ }
1304
+ return this.cache.notFoundRoute;
1023
1305
  }
1024
1306
  loadErrorRoute() {
1025
- return loadErrorRouteFromFilesystem(this.appDir);
1307
+ this.ensureCacheValid();
1308
+ if (!this.cache) {
1309
+ this.loadRoutes();
1310
+ }
1311
+ if (!this.cache) {
1312
+ throw new Error("Failed to initialize route cache");
1313
+ }
1314
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.errorRoute === void 0) {
1315
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1316
+ const fileStats = buildFileStats(files);
1317
+ this.cache.errorRoute = loadErrorRouteFromFilesystem(this.appDir);
1318
+ this.cache.fileStats = fileStats;
1319
+ this.cache.timestamp = Date.now();
1320
+ }
1321
+ return this.cache.errorRoute;
1026
1322
  }
1027
1323
  loadRouteChunks() {
1028
1324
  return {};
@@ -1031,27 +1327,67 @@ var FilesystemRouteLoader = class {
1031
1327
  var ManifestRouteLoader = class {
1032
1328
  constructor(projectRoot) {
1033
1329
  this.projectRoot = projectRoot;
1330
+ this.cache = {};
1331
+ }
1332
+ /**
1333
+ * Gets the manifest, using cache if available.
1334
+ * The manifest is read once and cached for the lifetime of the loader.
1335
+ */
1336
+ getManifest() {
1337
+ if (this.cache.manifest !== void 0) {
1338
+ return this.cache.manifest;
1339
+ }
1340
+ const manifest = readManifest(this.projectRoot);
1341
+ this.cache.manifest = manifest;
1342
+ return manifest;
1034
1343
  }
1035
1344
  loadRoutes() {
1345
+ if (this.cache.routes) {
1346
+ return this.cache.routes;
1347
+ }
1036
1348
  const { routes } = loadRoutesFromManifest(this.projectRoot);
1349
+ this.cache.routes = routes;
1037
1350
  return routes;
1038
1351
  }
1039
1352
  loadApiRoutes() {
1353
+ if (this.cache.apiRoutes) {
1354
+ return this.cache.apiRoutes;
1355
+ }
1040
1356
  const { apiRoutes } = loadRoutesFromManifest(this.projectRoot);
1357
+ this.cache.apiRoutes = apiRoutes;
1041
1358
  return apiRoutes;
1042
1359
  }
1043
1360
  loadWssRoutes() {
1361
+ if (this.cache.wssRoutes) {
1362
+ return this.cache.wssRoutes;
1363
+ }
1044
1364
  const { wssRoutes } = loadRoutesFromManifest(this.projectRoot);
1365
+ this.cache.wssRoutes = wssRoutes;
1045
1366
  return wssRoutes;
1046
1367
  }
1047
1368
  loadNotFoundRoute() {
1048
- return loadNotFoundFromManifest(this.projectRoot);
1369
+ if (this.cache.notFoundRoute !== void 0) {
1370
+ return this.cache.notFoundRoute;
1371
+ }
1372
+ const route = loadNotFoundFromManifest(this.projectRoot);
1373
+ this.cache.notFoundRoute = route;
1374
+ return route;
1049
1375
  }
1050
1376
  loadErrorRoute() {
1051
- return loadErrorFromManifest(this.projectRoot);
1377
+ if (this.cache.errorRoute !== void 0) {
1378
+ return this.cache.errorRoute;
1379
+ }
1380
+ const route = loadErrorFromManifest(this.projectRoot);
1381
+ this.cache.errorRoute = route;
1382
+ return route;
1052
1383
  }
1053
1384
  loadRouteChunks() {
1054
- return loadChunksFromManifest(this.projectRoot);
1385
+ if (this.cache.routeChunks) {
1386
+ return this.cache.routeChunks;
1387
+ }
1388
+ const chunks = loadChunksFromManifest(this.projectRoot);
1389
+ this.cache.routeChunks = chunks;
1390
+ return chunks;
1055
1391
  }
1056
1392
  };
1057
1393
  function loadNotFoundRouteFromFilesystem(appDir) {
@@ -1086,7 +1422,12 @@ function loadNotFoundRouteFromFilesystem(appDir) {
1086
1422
  notFoundDir,
1087
1423
  appDir
1088
1424
  );
1089
- const { middlewares, loader, dynamic, generateStaticParams } = loadLoaderForDir(notFoundDir);
1425
+ const layoutServerHooks = [];
1426
+ for (const layoutFile of layoutFiles) {
1427
+ const layoutServerHook = loadLayoutServerHook(layoutFile);
1428
+ layoutServerHooks.push(layoutServerHook);
1429
+ }
1430
+ const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(notFoundDir);
1090
1431
  return {
1091
1432
  pattern: NOT_FOUND_PATTERN,
1092
1433
  regex: new RegExp(`^${NOT_FOUND_PATTERN}/?$`),
@@ -1096,7 +1437,10 @@ function loadNotFoundRouteFromFilesystem(appDir) {
1096
1437
  pageFile: notFoundFile,
1097
1438
  layoutFiles,
1098
1439
  middlewares,
1099
- loader,
1440
+ loader: serverHook,
1441
+ // Keep 'loader' field name for backward compatibility
1442
+ layoutServerHooks,
1443
+ // Server hooks for each layout (same order as layouts)
1100
1444
  dynamic,
1101
1445
  generateStaticParams
1102
1446
  };
@@ -1127,7 +1471,12 @@ function loadErrorRouteFromFilesystem(appDir) {
1127
1471
  appDir,
1128
1472
  appDir
1129
1473
  );
1130
- const { middlewares, loader, dynamic, generateStaticParams } = loadLoaderForDir(appDir);
1474
+ const layoutServerHooks = [];
1475
+ for (const layoutFile of layoutFiles) {
1476
+ const layoutServerHook = loadLayoutServerHook(layoutFile);
1477
+ layoutServerHooks.push(layoutServerHook);
1478
+ }
1479
+ const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(appDir);
1131
1480
  return {
1132
1481
  pattern: ERROR_PATTERN,
1133
1482
  regex: new RegExp(`^${ERROR_PATTERN}/?$`),
@@ -1137,7 +1486,10 @@ function loadErrorRouteFromFilesystem(appDir) {
1137
1486
  pageFile: errorFile,
1138
1487
  layoutFiles,
1139
1488
  middlewares,
1140
- loader,
1489
+ loader: serverHook,
1490
+ // Keep 'loader' field name for backward compatibility
1491
+ layoutServerHooks,
1492
+ // Server hooks for each layout (same order as layouts)
1141
1493
  dynamic,
1142
1494
  generateStaticParams
1143
1495
  };
@@ -1168,7 +1520,8 @@ function loadAliasesFromTsconfig(projectRoot) {
1168
1520
  try {
1169
1521
  tsconfig = JSON.parse(fs11.readFileSync(tsconfigPath, "utf-8"));
1170
1522
  } catch (err) {
1171
- console.warn("[framework] Could not read tsconfig.json:", err);
1523
+ console.warn("\u26A0\uFE0F [framework] Could not read tsconfig.json:", err instanceof Error ? err.message : String(err));
1524
+ console.warn("\u{1F4A1} Using default path aliases. For custom aliases, ensure tsconfig.json is valid.");
1172
1525
  aliases["@app"] = path11.resolve(projectRoot, "app");
1173
1526
  return aliases;
1174
1527
  }
@@ -1222,7 +1575,7 @@ function copyStaticAssets(projectRoot, outDir) {
1222
1575
  }
1223
1576
  }
1224
1577
  }
1225
- function generateAssetManifest(outDir) {
1578
+ function generateAssetManifest(outDir, stats) {
1226
1579
  const manifest = {
1227
1580
  client: {
1228
1581
  js: "client.js",
@@ -1234,27 +1587,128 @@ function generateAssetManifest(outDir) {
1234
1587
  return manifest;
1235
1588
  }
1236
1589
  const files = fs11.readdirSync(outDir);
1237
- const clientJsMatch = files.find((f) => /^client\.[\w-]+\.js$/.test(f) || f === "client.js");
1238
- if (clientJsMatch) {
1239
- manifest.client.js = clientJsMatch;
1590
+ if (stats) {
1591
+ try {
1592
+ const statsJson = stats.toJson({
1593
+ all: false,
1594
+ entrypoints: true,
1595
+ assets: true,
1596
+ chunks: true,
1597
+ chunkRelations: true
1598
+ // Include chunk dependencies
1599
+ });
1600
+ const clientEntrypoint = statsJson.entrypoints?.client;
1601
+ if (clientEntrypoint?.assets) {
1602
+ const clientJsFiles = clientEntrypoint.assets.map((asset) => typeof asset === "string" ? asset : asset.name).filter((name) => name.endsWith(".js"));
1603
+ if (statsJson.chunks && clientEntrypoint.chunks) {
1604
+ const entrypointChunkIds = new Set(
1605
+ Array.isArray(clientEntrypoint.chunks) ? clientEntrypoint.chunks : [clientEntrypoint.chunks]
1606
+ );
1607
+ const dependencyChunks = [];
1608
+ for (const chunk of statsJson.chunks) {
1609
+ if (chunk.id && entrypointChunkIds.has(chunk.id)) {
1610
+ if (chunk.files) {
1611
+ const jsFiles = chunk.files.filter((f) => f.endsWith(".js")).filter((f) => !clientJsFiles.includes(f));
1612
+ dependencyChunks.push(...jsFiles);
1613
+ }
1614
+ }
1615
+ }
1616
+ const visitedChunkIds = new Set(entrypointChunkIds);
1617
+ const chunksToCheck = Array.from(entrypointChunkIds);
1618
+ while (chunksToCheck.length > 0) {
1619
+ const chunkId = chunksToCheck.shift();
1620
+ if (!chunkId) continue;
1621
+ const chunk = statsJson.chunks?.find((c) => c.id === chunkId);
1622
+ if (chunk?.children) {
1623
+ const children = Array.isArray(chunk.children) ? chunk.children : [chunk.children];
1624
+ for (const childId of children) {
1625
+ if (!visitedChunkIds.has(childId)) {
1626
+ visitedChunkIds.add(childId);
1627
+ chunksToCheck.push(childId);
1628
+ const childChunk = statsJson.chunks?.find((c) => c.id === childId);
1629
+ if (childChunk?.files) {
1630
+ const jsFiles = childChunk.files.filter((f) => f.endsWith(".js")).filter((f) => !clientJsFiles.includes(f) && !dependencyChunks.includes(f));
1631
+ dependencyChunks.push(...jsFiles);
1632
+ }
1633
+ }
1634
+ }
1635
+ }
1636
+ }
1637
+ if (dependencyChunks.length > 0) {
1638
+ clientJsFiles.splice(-1, 0, ...dependencyChunks);
1639
+ }
1640
+ }
1641
+ if (clientJsFiles.length > 0) {
1642
+ manifest.entrypoints = {
1643
+ client: clientJsFiles
1644
+ };
1645
+ manifest.client.js = clientJsFiles[clientJsFiles.length - 1];
1646
+ const clientCssFiles = clientEntrypoint.assets.map((asset) => typeof asset === "string" ? asset : asset.name).filter((name) => name.endsWith(".css"));
1647
+ if (clientCssFiles.length > 0) {
1648
+ manifest.client.css = clientCssFiles[0];
1649
+ }
1650
+ }
1651
+ }
1652
+ } catch (err) {
1653
+ console.warn("[framework] Failed to extract entrypoints from stats, falling back to file scanning:", err);
1654
+ }
1655
+ }
1656
+ if (!manifest.client.js) {
1657
+ const clientJsMatch = files.find((f) => /^client\.[\w-]+\.js$/.test(f) || f === "client.js");
1658
+ if (clientJsMatch) {
1659
+ manifest.client.js = clientJsMatch;
1660
+ }
1240
1661
  }
1241
- const clientCssMatch = files.find((f) => /^client\.[\w-]+\.css$/.test(f) || f === "client.css");
1242
- if (clientCssMatch) {
1243
- manifest.client.css = clientCssMatch;
1662
+ if (!manifest.client.css) {
1663
+ const clientCssMatch = files.find((f) => /^client\.[\w-]+\.css$/.test(f) || f === "client.css");
1664
+ if (clientCssMatch) {
1665
+ manifest.client.css = clientCssMatch;
1666
+ }
1244
1667
  }
1668
+ const sharedChunksToAdd = [];
1245
1669
  for (const file of files) {
1246
1670
  if (!file.endsWith(".js")) continue;
1247
1671
  if (file === manifest.client.js) continue;
1672
+ if (manifest.entrypoints?.client?.includes(file)) continue;
1248
1673
  const routeMatch = file.match(/^(route-[^.]+)(\.[\w-]+)?\.js$/);
1249
1674
  if (routeMatch) {
1250
1675
  const chunkName = routeMatch[1];
1251
1676
  manifest.chunks[chunkName] = file;
1252
1677
  continue;
1253
1678
  }
1679
+ const vendorMatch = file.match(/^(vendor)(\.[\w-]+)?\.js$/);
1680
+ if (vendorMatch) {
1681
+ const chunkName = vendorMatch[1];
1682
+ manifest.chunks[chunkName] = file;
1683
+ sharedChunksToAdd.push(file);
1684
+ continue;
1685
+ }
1686
+ const vendorCommonsMatch = file.match(/^(vendor-commons)(\.[\w-]+)?\.js$/);
1687
+ if (vendorCommonsMatch) {
1688
+ const chunkName = vendorCommonsMatch[1];
1689
+ manifest.chunks[chunkName] = file;
1690
+ sharedChunksToAdd.push(file);
1691
+ continue;
1692
+ }
1254
1693
  const numericMatch = file.match(/^(\d+)(\.[\w-]+)?\.js$/);
1255
1694
  if (numericMatch) {
1256
1695
  const chunkName = numericMatch[1];
1257
1696
  manifest.chunks[chunkName] = file;
1697
+ sharedChunksToAdd.push(file);
1698
+ continue;
1699
+ }
1700
+ }
1701
+ if (sharedChunksToAdd.length > 0 && manifest.entrypoints?.client) {
1702
+ const entrypoints = manifest.entrypoints.client;
1703
+ const mainEntry = entrypoints[entrypoints.length - 1];
1704
+ const uniqueShared = sharedChunksToAdd.filter((f) => !entrypoints.includes(f));
1705
+ entrypoints.splice(-1, 0, ...uniqueShared);
1706
+ if (entrypoints[entrypoints.length - 1] !== mainEntry) {
1707
+ const mainIndex = entrypoints.indexOf(mainEntry);
1708
+ if (mainIndex >= 0) {
1709
+ entrypoints.splice(mainIndex, 1);
1710
+ }
1711
+ entrypoints.push(mainEntry);
1258
1712
  }
1259
1713
  }
1260
1714
  return manifest;
@@ -1294,9 +1748,7 @@ function createClientConfig(projectRoot, mode) {
1294
1748
  const outDir = path12.join(buildDir, "client");
1295
1749
  const envPath2 = path12.join(projectRoot, ".env");
1296
1750
  if (fs12.existsSync(envPath2)) {
1297
- dotenv.config({
1298
- path: envPath2
1299
- });
1751
+ dotenv.config({ path: envPath2 });
1300
1752
  }
1301
1753
  const publicEnv = {};
1302
1754
  for (const [key, value] of Object.entries(process.env)) {
@@ -1355,14 +1807,57 @@ function createClientConfig(projectRoot, mode) {
1355
1807
  },
1356
1808
  plugins: [
1357
1809
  new rspack.DefinePlugin({
1358
- // Always define NODE_ENV, using mode as fallback if not set
1359
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || mode),
1810
+ // Use mode directly to ensure development mode is correctly set
1811
+ // This replaces process.env.NODE_ENV in the client bundle with the literal string value
1812
+ "process.env.NODE_ENV": JSON.stringify(mode),
1360
1813
  ...publicEnv
1361
1814
  }),
1362
1815
  new rspack.CssExtractRspackPlugin({
1363
1816
  filename: mode === "production" ? "client.[contenthash].css" : "client.css"
1364
1817
  })
1365
1818
  ],
1819
+ optimization: mode === "production" ? {
1820
+ usedExports: true,
1821
+ sideEffects: false,
1822
+ // More aggressive tree shaking - assume no side effects
1823
+ providedExports: true,
1824
+ concatenateModules: true,
1825
+ // Better for tree shaking
1826
+ minimize: true,
1827
+ removeEmptyChunks: true,
1828
+ mergeDuplicateChunks: true,
1829
+ // Improved code splitting: separate vendor chunks for better caching
1830
+ splitChunks: {
1831
+ chunks: "all",
1832
+ cacheGroups: {
1833
+ // Separate React/React-DOM into dedicated vendor chunk
1834
+ // This improves caching: React rarely changes, so users don't need to re-download it
1835
+ vendor: {
1836
+ test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
1837
+ name: "vendor",
1838
+ priority: 30,
1839
+ enforce: true,
1840
+ // Force separation even if only used once
1841
+ reuseExistingChunk: true
1842
+ },
1843
+ // Other node_modules dependencies in a separate chunk
1844
+ vendorCommons: {
1845
+ test: /[\\/]node_modules[\\/](?!(react|react-dom|scheduler)[\\/])/,
1846
+ name: "vendor-commons",
1847
+ priority: 10,
1848
+ minChunks: 2,
1849
+ // Only create if used in 2+ chunks
1850
+ reuseExistingChunk: true
1851
+ },
1852
+ // Default: shared application code (not in node_modules)
1853
+ default: {
1854
+ minChunks: 2,
1855
+ priority: 5,
1856
+ reuseExistingChunk: true
1857
+ }
1858
+ }
1859
+ }
1860
+ } : void 0,
1366
1861
  infrastructureLogging: {
1367
1862
  level: "error"
1368
1863
  },
@@ -1391,7 +1886,13 @@ function startClientBundler(projectRoot, mode = "development") {
1391
1886
  });
1392
1887
  compiler.watch({}, (err, stats) => {
1393
1888
  if (err) {
1394
- console.error("[framework][client] Rspack error:", err);
1889
+ console.error("\n\u274C [framework][client] Rspack compilation error:");
1890
+ console.error(err);
1891
+ console.error("\n\u{1F4A1} Suggestions:");
1892
+ console.error(" \u2022 Check for syntax errors in your code");
1893
+ console.error(" \u2022 Verify all imports are correct");
1894
+ console.error(" \u2022 Ensure all dependencies are installed");
1895
+ console.error(" \u2022 Try deleting .loly folder and rebuilding\n");
1395
1896
  isBuilding = false;
1396
1897
  lastBuildTime = Date.now();
1397
1898
  if (buildResolve) {
@@ -1402,17 +1903,20 @@ function startClientBundler(projectRoot, mode = "development") {
1402
1903
  return;
1403
1904
  }
1404
1905
  if (!stats) {
1906
+ console.warn("\u26A0\uFE0F [framework][client] Build completed but no stats available");
1405
1907
  isBuilding = false;
1406
1908
  lastBuildTime = Date.now();
1407
1909
  return;
1408
1910
  }
1409
1911
  if (stats.hasErrors()) {
1410
- console.error(
1411
- "[framework][client] Build with errors:\n",
1412
- stats.toString("errors-only")
1413
- );
1912
+ console.error("\n\u274C [framework][client] Build failed with errors:\n");
1913
+ console.error(stats.toString("errors-only"));
1914
+ console.error("\n\u{1F4A1} Common fixes:");
1915
+ console.error(" \u2022 Fix syntax errors shown above");
1916
+ console.error(" \u2022 Check for missing imports or dependencies");
1917
+ console.error(" \u2022 Verify TypeScript types are correct\n");
1414
1918
  } else {
1415
- console.log("[framework][client] \u2713 Client bundle rebuilt successfully");
1919
+ console.log("\u2705 [framework][client] Client bundle rebuilt successfully");
1416
1920
  }
1417
1921
  isBuilding = false;
1418
1922
  lastBuildTime = Date.now();
@@ -1447,23 +1951,33 @@ function buildClientBundle(projectRoot) {
1447
1951
  compiler.close(() => {
1448
1952
  });
1449
1953
  if (err) {
1450
- console.error("[framework][client] Build error:", err);
1954
+ console.error("\n\u274C [framework][client] Production build error:");
1955
+ console.error(err);
1956
+ console.error("\n\u{1F4A1} Suggestions:");
1957
+ console.error(" \u2022 Check for syntax errors in your code");
1958
+ console.error(" \u2022 Verify all imports are correct");
1959
+ console.error(" \u2022 Ensure all dependencies are installed");
1960
+ console.error(" \u2022 Review the error details above\n");
1451
1961
  return reject(err);
1452
1962
  }
1453
1963
  if (!stats) {
1454
- const error = new Error("No stats from Rspack");
1455
- console.error("[framework][client] Build error:", error);
1964
+ const error = new Error("No stats from Rspack - build may have failed silently");
1965
+ console.error("\n\u274C [framework][client] Build error:", error.message);
1966
+ console.error("\u{1F4A1} Try rebuilding or check Rspack configuration\n");
1456
1967
  return reject(error);
1457
1968
  }
1458
1969
  if (stats.hasErrors()) {
1459
- console.error(
1460
- "[framework][client] Build with errors:\n",
1461
- stats.toString("errors-only")
1462
- );
1463
- return reject(new Error("Client build failed"));
1970
+ console.error("\n\u274C [framework][client] Production build failed:\n");
1971
+ console.error(stats.toString("errors-only"));
1972
+ console.error("\n\u{1F4A1} Common fixes:");
1973
+ console.error(" \u2022 Fix syntax errors shown above");
1974
+ console.error(" \u2022 Check for missing imports or dependencies");
1975
+ console.error(" \u2022 Verify TypeScript types are correct");
1976
+ console.error(" \u2022 Review build configuration\n");
1977
+ return reject(new Error("Client build failed - see errors above"));
1464
1978
  }
1465
1979
  copyStaticAssets(projectRoot, outDir);
1466
- const assetManifest = generateAssetManifest(outDir);
1980
+ const assetManifest = generateAssetManifest(outDir, stats);
1467
1981
  const manifestPath = path13.join(projectRoot, BUILD_FOLDER_NAME, "asset-manifest.json");
1468
1982
  fs13.writeFileSync(manifestPath, JSON.stringify(assetManifest, null, 2), "utf-8");
1469
1983
  resolve3({ outDir });
@@ -1547,7 +2061,7 @@ var ReaddirpStream = class extends Readable {
1547
2061
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
1548
2062
  const statMethod = opts.lstat ? lstat : stat;
1549
2063
  if (wantBigintFsStats) {
1550
- this._stat = (path25) => statMethod(path25, { bigint: true });
2064
+ this._stat = (path28) => statMethod(path28, { bigint: true });
1551
2065
  } else {
1552
2066
  this._stat = statMethod;
1553
2067
  }
@@ -1572,8 +2086,8 @@ var ReaddirpStream = class extends Readable {
1572
2086
  const par = this.parent;
1573
2087
  const fil = par && par.files;
1574
2088
  if (fil && fil.length > 0) {
1575
- const { path: path25, depth } = par;
1576
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path25));
2089
+ const { path: path28, depth } = par;
2090
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path28));
1577
2091
  const awaited = await Promise.all(slice);
1578
2092
  for (const entry of awaited) {
1579
2093
  if (!entry)
@@ -1613,20 +2127,20 @@ var ReaddirpStream = class extends Readable {
1613
2127
  this.reading = false;
1614
2128
  }
1615
2129
  }
1616
- async _exploreDir(path25, depth) {
2130
+ async _exploreDir(path28, depth) {
1617
2131
  let files;
1618
2132
  try {
1619
- files = await readdir(path25, this._rdOptions);
2133
+ files = await readdir(path28, this._rdOptions);
1620
2134
  } catch (error) {
1621
2135
  this._onError(error);
1622
2136
  }
1623
- return { files, depth, path: path25 };
2137
+ return { files, depth, path: path28 };
1624
2138
  }
1625
- async _formatEntry(dirent, path25) {
2139
+ async _formatEntry(dirent, path28) {
1626
2140
  let entry;
1627
2141
  const basename3 = this._isDirent ? dirent.name : dirent;
1628
2142
  try {
1629
- const fullPath = presolve(pjoin(path25, basename3));
2143
+ const fullPath = presolve(pjoin(path28, basename3));
1630
2144
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
1631
2145
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
1632
2146
  } catch (err) {
@@ -2026,16 +2540,16 @@ var delFromSet = (main, prop, item) => {
2026
2540
  };
2027
2541
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
2028
2542
  var FsWatchInstances = /* @__PURE__ */ new Map();
2029
- function createFsWatchInstance(path25, options, listener, errHandler, emitRaw) {
2543
+ function createFsWatchInstance(path28, options, listener, errHandler, emitRaw) {
2030
2544
  const handleEvent = (rawEvent, evPath) => {
2031
- listener(path25);
2032
- emitRaw(rawEvent, evPath, { watchedPath: path25 });
2033
- if (evPath && path25 !== evPath) {
2034
- fsWatchBroadcast(sysPath.resolve(path25, evPath), KEY_LISTENERS, sysPath.join(path25, evPath));
2545
+ listener(path28);
2546
+ emitRaw(rawEvent, evPath, { watchedPath: path28 });
2547
+ if (evPath && path28 !== evPath) {
2548
+ fsWatchBroadcast(sysPath.resolve(path28, evPath), KEY_LISTENERS, sysPath.join(path28, evPath));
2035
2549
  }
2036
2550
  };
2037
2551
  try {
2038
- return fs_watch(path25, {
2552
+ return fs_watch(path28, {
2039
2553
  persistent: options.persistent
2040
2554
  }, handleEvent);
2041
2555
  } catch (error) {
@@ -2051,12 +2565,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
2051
2565
  listener(val1, val2, val3);
2052
2566
  });
2053
2567
  };
2054
- var setFsWatchListener = (path25, fullPath, options, handlers) => {
2568
+ var setFsWatchListener = (path28, fullPath, options, handlers) => {
2055
2569
  const { listener, errHandler, rawEmitter } = handlers;
2056
2570
  let cont = FsWatchInstances.get(fullPath);
2057
2571
  let watcher;
2058
2572
  if (!options.persistent) {
2059
- watcher = createFsWatchInstance(path25, options, listener, errHandler, rawEmitter);
2573
+ watcher = createFsWatchInstance(path28, options, listener, errHandler, rawEmitter);
2060
2574
  if (!watcher)
2061
2575
  return;
2062
2576
  return watcher.close.bind(watcher);
@@ -2067,7 +2581,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2067
2581
  addAndConvert(cont, KEY_RAW, rawEmitter);
2068
2582
  } else {
2069
2583
  watcher = createFsWatchInstance(
2070
- path25,
2584
+ path28,
2071
2585
  options,
2072
2586
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
2073
2587
  errHandler,
@@ -2082,7 +2596,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2082
2596
  cont.watcherUnusable = true;
2083
2597
  if (isWindows && error.code === "EPERM") {
2084
2598
  try {
2085
- const fd = await open(path25, "r");
2599
+ const fd = await open(path28, "r");
2086
2600
  await fd.close();
2087
2601
  broadcastErr(error);
2088
2602
  } catch (err) {
@@ -2113,7 +2627,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2113
2627
  };
2114
2628
  };
2115
2629
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
2116
- var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
2630
+ var setFsWatchFileListener = (path28, fullPath, options, handlers) => {
2117
2631
  const { listener, rawEmitter } = handlers;
2118
2632
  let cont = FsWatchFileInstances.get(fullPath);
2119
2633
  const copts = cont && cont.options;
@@ -2135,7 +2649,7 @@ var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
2135
2649
  });
2136
2650
  const currmtime = curr.mtimeMs;
2137
2651
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
2138
- foreach(cont.listeners, (listener2) => listener2(path25, curr));
2652
+ foreach(cont.listeners, (listener2) => listener2(path28, curr));
2139
2653
  }
2140
2654
  })
2141
2655
  };
@@ -2163,13 +2677,13 @@ var NodeFsHandler = class {
2163
2677
  * @param listener on fs change
2164
2678
  * @returns closer for the watcher instance
2165
2679
  */
2166
- _watchWithNodeFs(path25, listener) {
2680
+ _watchWithNodeFs(path28, listener) {
2167
2681
  const opts = this.fsw.options;
2168
- const directory = sysPath.dirname(path25);
2169
- const basename3 = sysPath.basename(path25);
2682
+ const directory = sysPath.dirname(path28);
2683
+ const basename3 = sysPath.basename(path28);
2170
2684
  const parent = this.fsw._getWatchedDir(directory);
2171
2685
  parent.add(basename3);
2172
- const absolutePath = sysPath.resolve(path25);
2686
+ const absolutePath = sysPath.resolve(path28);
2173
2687
  const options = {
2174
2688
  persistent: opts.persistent
2175
2689
  };
@@ -2179,12 +2693,12 @@ var NodeFsHandler = class {
2179
2693
  if (opts.usePolling) {
2180
2694
  const enableBin = opts.interval !== opts.binaryInterval;
2181
2695
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
2182
- closer = setFsWatchFileListener(path25, absolutePath, options, {
2696
+ closer = setFsWatchFileListener(path28, absolutePath, options, {
2183
2697
  listener,
2184
2698
  rawEmitter: this.fsw._emitRaw
2185
2699
  });
2186
2700
  } else {
2187
- closer = setFsWatchListener(path25, absolutePath, options, {
2701
+ closer = setFsWatchListener(path28, absolutePath, options, {
2188
2702
  listener,
2189
2703
  errHandler: this._boundHandleError,
2190
2704
  rawEmitter: this.fsw._emitRaw
@@ -2206,7 +2720,7 @@ var NodeFsHandler = class {
2206
2720
  let prevStats = stats;
2207
2721
  if (parent.has(basename3))
2208
2722
  return;
2209
- const listener = async (path25, newStats) => {
2723
+ const listener = async (path28, newStats) => {
2210
2724
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
2211
2725
  return;
2212
2726
  if (!newStats || newStats.mtimeMs === 0) {
@@ -2220,11 +2734,11 @@ var NodeFsHandler = class {
2220
2734
  this.fsw._emit(EV.CHANGE, file, newStats2);
2221
2735
  }
2222
2736
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
2223
- this.fsw._closeFile(path25);
2737
+ this.fsw._closeFile(path28);
2224
2738
  prevStats = newStats2;
2225
2739
  const closer2 = this._watchWithNodeFs(file, listener);
2226
2740
  if (closer2)
2227
- this.fsw._addPathCloser(path25, closer2);
2741
+ this.fsw._addPathCloser(path28, closer2);
2228
2742
  } else {
2229
2743
  prevStats = newStats2;
2230
2744
  }
@@ -2256,7 +2770,7 @@ var NodeFsHandler = class {
2256
2770
  * @param item basename of this item
2257
2771
  * @returns true if no more processing is needed for this entry.
2258
2772
  */
2259
- async _handleSymlink(entry, directory, path25, item) {
2773
+ async _handleSymlink(entry, directory, path28, item) {
2260
2774
  if (this.fsw.closed) {
2261
2775
  return;
2262
2776
  }
@@ -2266,7 +2780,7 @@ var NodeFsHandler = class {
2266
2780
  this.fsw._incrReadyCount();
2267
2781
  let linkPath;
2268
2782
  try {
2269
- linkPath = await fsrealpath(path25);
2783
+ linkPath = await fsrealpath(path28);
2270
2784
  } catch (e) {
2271
2785
  this.fsw._emitReady();
2272
2786
  return true;
@@ -2276,12 +2790,12 @@ var NodeFsHandler = class {
2276
2790
  if (dir.has(item)) {
2277
2791
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
2278
2792
  this.fsw._symlinkPaths.set(full, linkPath);
2279
- this.fsw._emit(EV.CHANGE, path25, entry.stats);
2793
+ this.fsw._emit(EV.CHANGE, path28, entry.stats);
2280
2794
  }
2281
2795
  } else {
2282
2796
  dir.add(item);
2283
2797
  this.fsw._symlinkPaths.set(full, linkPath);
2284
- this.fsw._emit(EV.ADD, path25, entry.stats);
2798
+ this.fsw._emit(EV.ADD, path28, entry.stats);
2285
2799
  }
2286
2800
  this.fsw._emitReady();
2287
2801
  return true;
@@ -2310,9 +2824,9 @@ var NodeFsHandler = class {
2310
2824
  return;
2311
2825
  }
2312
2826
  const item = entry.path;
2313
- let path25 = sysPath.join(directory, item);
2827
+ let path28 = sysPath.join(directory, item);
2314
2828
  current.add(item);
2315
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path25, item)) {
2829
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path28, item)) {
2316
2830
  return;
2317
2831
  }
2318
2832
  if (this.fsw.closed) {
@@ -2321,8 +2835,8 @@ var NodeFsHandler = class {
2321
2835
  }
2322
2836
  if (item === target || !target && !previous.has(item)) {
2323
2837
  this.fsw._incrReadyCount();
2324
- path25 = sysPath.join(dir, sysPath.relative(dir, path25));
2325
- this._addToNodeFs(path25, initialAdd, wh, depth + 1);
2838
+ path28 = sysPath.join(dir, sysPath.relative(dir, path28));
2839
+ this._addToNodeFs(path28, initialAdd, wh, depth + 1);
2326
2840
  }
2327
2841
  }).on(EV.ERROR, this._boundHandleError);
2328
2842
  return new Promise((resolve3, reject) => {
@@ -2391,13 +2905,13 @@ var NodeFsHandler = class {
2391
2905
  * @param depth Child path actually targeted for watch
2392
2906
  * @param target Child path actually targeted for watch
2393
2907
  */
2394
- async _addToNodeFs(path25, initialAdd, priorWh, depth, target) {
2908
+ async _addToNodeFs(path28, initialAdd, priorWh, depth, target) {
2395
2909
  const ready = this.fsw._emitReady;
2396
- if (this.fsw._isIgnored(path25) || this.fsw.closed) {
2910
+ if (this.fsw._isIgnored(path28) || this.fsw.closed) {
2397
2911
  ready();
2398
2912
  return false;
2399
2913
  }
2400
- const wh = this.fsw._getWatchHelpers(path25);
2914
+ const wh = this.fsw._getWatchHelpers(path28);
2401
2915
  if (priorWh) {
2402
2916
  wh.filterPath = (entry) => priorWh.filterPath(entry);
2403
2917
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -2413,8 +2927,8 @@ var NodeFsHandler = class {
2413
2927
  const follow = this.fsw.options.followSymlinks;
2414
2928
  let closer;
2415
2929
  if (stats.isDirectory()) {
2416
- const absPath = sysPath.resolve(path25);
2417
- const targetPath = follow ? await fsrealpath(path25) : path25;
2930
+ const absPath = sysPath.resolve(path28);
2931
+ const targetPath = follow ? await fsrealpath(path28) : path28;
2418
2932
  if (this.fsw.closed)
2419
2933
  return;
2420
2934
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -2424,29 +2938,29 @@ var NodeFsHandler = class {
2424
2938
  this.fsw._symlinkPaths.set(absPath, targetPath);
2425
2939
  }
2426
2940
  } else if (stats.isSymbolicLink()) {
2427
- const targetPath = follow ? await fsrealpath(path25) : path25;
2941
+ const targetPath = follow ? await fsrealpath(path28) : path28;
2428
2942
  if (this.fsw.closed)
2429
2943
  return;
2430
2944
  const parent = sysPath.dirname(wh.watchPath);
2431
2945
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
2432
2946
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
2433
- closer = await this._handleDir(parent, stats, initialAdd, depth, path25, wh, targetPath);
2947
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path28, wh, targetPath);
2434
2948
  if (this.fsw.closed)
2435
2949
  return;
2436
2950
  if (targetPath !== void 0) {
2437
- this.fsw._symlinkPaths.set(sysPath.resolve(path25), targetPath);
2951
+ this.fsw._symlinkPaths.set(sysPath.resolve(path28), targetPath);
2438
2952
  }
2439
2953
  } else {
2440
2954
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
2441
2955
  }
2442
2956
  ready();
2443
2957
  if (closer)
2444
- this.fsw._addPathCloser(path25, closer);
2958
+ this.fsw._addPathCloser(path28, closer);
2445
2959
  return false;
2446
2960
  } catch (error) {
2447
2961
  if (this.fsw._handleError(error)) {
2448
2962
  ready();
2449
- return path25;
2963
+ return path28;
2450
2964
  }
2451
2965
  }
2452
2966
  }
@@ -2489,26 +3003,26 @@ function createPattern(matcher) {
2489
3003
  }
2490
3004
  return () => false;
2491
3005
  }
2492
- function normalizePath(path25) {
2493
- if (typeof path25 !== "string")
3006
+ function normalizePath(path28) {
3007
+ if (typeof path28 !== "string")
2494
3008
  throw new Error("string expected");
2495
- path25 = sysPath2.normalize(path25);
2496
- path25 = path25.replace(/\\/g, "/");
3009
+ path28 = sysPath2.normalize(path28);
3010
+ path28 = path28.replace(/\\/g, "/");
2497
3011
  let prepend = false;
2498
- if (path25.startsWith("//"))
3012
+ if (path28.startsWith("//"))
2499
3013
  prepend = true;
2500
3014
  const DOUBLE_SLASH_RE2 = /\/\//;
2501
- while (path25.match(DOUBLE_SLASH_RE2))
2502
- path25 = path25.replace(DOUBLE_SLASH_RE2, "/");
3015
+ while (path28.match(DOUBLE_SLASH_RE2))
3016
+ path28 = path28.replace(DOUBLE_SLASH_RE2, "/");
2503
3017
  if (prepend)
2504
- path25 = "/" + path25;
2505
- return path25;
3018
+ path28 = "/" + path28;
3019
+ return path28;
2506
3020
  }
2507
3021
  function matchPatterns(patterns, testString, stats) {
2508
- const path25 = normalizePath(testString);
3022
+ const path28 = normalizePath(testString);
2509
3023
  for (let index = 0; index < patterns.length; index++) {
2510
3024
  const pattern = patterns[index];
2511
- if (pattern(path25, stats)) {
3025
+ if (pattern(path28, stats)) {
2512
3026
  return true;
2513
3027
  }
2514
3028
  }
@@ -2548,19 +3062,19 @@ var toUnix = (string) => {
2548
3062
  }
2549
3063
  return str;
2550
3064
  };
2551
- var normalizePathToUnix = (path25) => toUnix(sysPath2.normalize(toUnix(path25)));
2552
- var normalizeIgnored = (cwd = "") => (path25) => {
2553
- if (typeof path25 === "string") {
2554
- return normalizePathToUnix(sysPath2.isAbsolute(path25) ? path25 : sysPath2.join(cwd, path25));
3065
+ var normalizePathToUnix = (path28) => toUnix(sysPath2.normalize(toUnix(path28)));
3066
+ var normalizeIgnored = (cwd = "") => (path28) => {
3067
+ if (typeof path28 === "string") {
3068
+ return normalizePathToUnix(sysPath2.isAbsolute(path28) ? path28 : sysPath2.join(cwd, path28));
2555
3069
  } else {
2556
- return path25;
3070
+ return path28;
2557
3071
  }
2558
3072
  };
2559
- var getAbsolutePath = (path25, cwd) => {
2560
- if (sysPath2.isAbsolute(path25)) {
2561
- return path25;
3073
+ var getAbsolutePath = (path28, cwd) => {
3074
+ if (sysPath2.isAbsolute(path28)) {
3075
+ return path28;
2562
3076
  }
2563
- return sysPath2.join(cwd, path25);
3077
+ return sysPath2.join(cwd, path28);
2564
3078
  };
2565
3079
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
2566
3080
  var DirEntry = class {
@@ -2615,10 +3129,10 @@ var DirEntry = class {
2615
3129
  var STAT_METHOD_F = "stat";
2616
3130
  var STAT_METHOD_L = "lstat";
2617
3131
  var WatchHelper = class {
2618
- constructor(path25, follow, fsw) {
3132
+ constructor(path28, follow, fsw) {
2619
3133
  this.fsw = fsw;
2620
- const watchPath = path25;
2621
- this.path = path25 = path25.replace(REPLACER_RE, "");
3134
+ const watchPath = path28;
3135
+ this.path = path28 = path28.replace(REPLACER_RE, "");
2622
3136
  this.watchPath = watchPath;
2623
3137
  this.fullWatchPath = sysPath2.resolve(watchPath);
2624
3138
  this.dirParts = [];
@@ -2740,20 +3254,20 @@ var FSWatcher = class extends EventEmitter {
2740
3254
  this._closePromise = void 0;
2741
3255
  let paths = unifyPaths(paths_);
2742
3256
  if (cwd) {
2743
- paths = paths.map((path25) => {
2744
- const absPath = getAbsolutePath(path25, cwd);
3257
+ paths = paths.map((path28) => {
3258
+ const absPath = getAbsolutePath(path28, cwd);
2745
3259
  return absPath;
2746
3260
  });
2747
3261
  }
2748
- paths.forEach((path25) => {
2749
- this._removeIgnoredPath(path25);
3262
+ paths.forEach((path28) => {
3263
+ this._removeIgnoredPath(path28);
2750
3264
  });
2751
3265
  this._userIgnored = void 0;
2752
3266
  if (!this._readyCount)
2753
3267
  this._readyCount = 0;
2754
3268
  this._readyCount += paths.length;
2755
- Promise.all(paths.map(async (path25) => {
2756
- const res = await this._nodeFsHandler._addToNodeFs(path25, !_internal, void 0, 0, _origAdd);
3269
+ Promise.all(paths.map(async (path28) => {
3270
+ const res = await this._nodeFsHandler._addToNodeFs(path28, !_internal, void 0, 0, _origAdd);
2757
3271
  if (res)
2758
3272
  this._emitReady();
2759
3273
  return res;
@@ -2775,17 +3289,17 @@ var FSWatcher = class extends EventEmitter {
2775
3289
  return this;
2776
3290
  const paths = unifyPaths(paths_);
2777
3291
  const { cwd } = this.options;
2778
- paths.forEach((path25) => {
2779
- if (!sysPath2.isAbsolute(path25) && !this._closers.has(path25)) {
3292
+ paths.forEach((path28) => {
3293
+ if (!sysPath2.isAbsolute(path28) && !this._closers.has(path28)) {
2780
3294
  if (cwd)
2781
- path25 = sysPath2.join(cwd, path25);
2782
- path25 = sysPath2.resolve(path25);
3295
+ path28 = sysPath2.join(cwd, path28);
3296
+ path28 = sysPath2.resolve(path28);
2783
3297
  }
2784
- this._closePath(path25);
2785
- this._addIgnoredPath(path25);
2786
- if (this._watched.has(path25)) {
3298
+ this._closePath(path28);
3299
+ this._addIgnoredPath(path28);
3300
+ if (this._watched.has(path28)) {
2787
3301
  this._addIgnoredPath({
2788
- path: path25,
3302
+ path: path28,
2789
3303
  recursive: true
2790
3304
  });
2791
3305
  }
@@ -2849,38 +3363,38 @@ var FSWatcher = class extends EventEmitter {
2849
3363
  * @param stats arguments to be passed with event
2850
3364
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
2851
3365
  */
2852
- async _emit(event, path25, stats) {
3366
+ async _emit(event, path28, stats) {
2853
3367
  if (this.closed)
2854
3368
  return;
2855
3369
  const opts = this.options;
2856
3370
  if (isWindows)
2857
- path25 = sysPath2.normalize(path25);
3371
+ path28 = sysPath2.normalize(path28);
2858
3372
  if (opts.cwd)
2859
- path25 = sysPath2.relative(opts.cwd, path25);
2860
- const args = [path25];
3373
+ path28 = sysPath2.relative(opts.cwd, path28);
3374
+ const args = [path28];
2861
3375
  if (stats != null)
2862
3376
  args.push(stats);
2863
3377
  const awf = opts.awaitWriteFinish;
2864
3378
  let pw;
2865
- if (awf && (pw = this._pendingWrites.get(path25))) {
3379
+ if (awf && (pw = this._pendingWrites.get(path28))) {
2866
3380
  pw.lastChange = /* @__PURE__ */ new Date();
2867
3381
  return this;
2868
3382
  }
2869
3383
  if (opts.atomic) {
2870
3384
  if (event === EVENTS.UNLINK) {
2871
- this._pendingUnlinks.set(path25, [event, ...args]);
3385
+ this._pendingUnlinks.set(path28, [event, ...args]);
2872
3386
  setTimeout(() => {
2873
- this._pendingUnlinks.forEach((entry, path26) => {
3387
+ this._pendingUnlinks.forEach((entry, path29) => {
2874
3388
  this.emit(...entry);
2875
3389
  this.emit(EVENTS.ALL, ...entry);
2876
- this._pendingUnlinks.delete(path26);
3390
+ this._pendingUnlinks.delete(path29);
2877
3391
  });
2878
3392
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
2879
3393
  return this;
2880
3394
  }
2881
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path25)) {
3395
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path28)) {
2882
3396
  event = EVENTS.CHANGE;
2883
- this._pendingUnlinks.delete(path25);
3397
+ this._pendingUnlinks.delete(path28);
2884
3398
  }
2885
3399
  }
2886
3400
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -2898,16 +3412,16 @@ var FSWatcher = class extends EventEmitter {
2898
3412
  this.emitWithAll(event, args);
2899
3413
  }
2900
3414
  };
2901
- this._awaitWriteFinish(path25, awf.stabilityThreshold, event, awfEmit);
3415
+ this._awaitWriteFinish(path28, awf.stabilityThreshold, event, awfEmit);
2902
3416
  return this;
2903
3417
  }
2904
3418
  if (event === EVENTS.CHANGE) {
2905
- const isThrottled = !this._throttle(EVENTS.CHANGE, path25, 50);
3419
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path28, 50);
2906
3420
  if (isThrottled)
2907
3421
  return this;
2908
3422
  }
2909
3423
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
2910
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path25) : path25;
3424
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path28) : path28;
2911
3425
  let stats2;
2912
3426
  try {
2913
3427
  stats2 = await stat3(fullPath);
@@ -2938,23 +3452,23 @@ var FSWatcher = class extends EventEmitter {
2938
3452
  * @param timeout duration of time to suppress duplicate actions
2939
3453
  * @returns tracking object or false if action should be suppressed
2940
3454
  */
2941
- _throttle(actionType, path25, timeout) {
3455
+ _throttle(actionType, path28, timeout) {
2942
3456
  if (!this._throttled.has(actionType)) {
2943
3457
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
2944
3458
  }
2945
3459
  const action = this._throttled.get(actionType);
2946
3460
  if (!action)
2947
3461
  throw new Error("invalid throttle");
2948
- const actionPath = action.get(path25);
3462
+ const actionPath = action.get(path28);
2949
3463
  if (actionPath) {
2950
3464
  actionPath.count++;
2951
3465
  return false;
2952
3466
  }
2953
3467
  let timeoutObject;
2954
3468
  const clear = () => {
2955
- const item = action.get(path25);
3469
+ const item = action.get(path28);
2956
3470
  const count = item ? item.count : 0;
2957
- action.delete(path25);
3471
+ action.delete(path28);
2958
3472
  clearTimeout(timeoutObject);
2959
3473
  if (item)
2960
3474
  clearTimeout(item.timeoutObject);
@@ -2962,7 +3476,7 @@ var FSWatcher = class extends EventEmitter {
2962
3476
  };
2963
3477
  timeoutObject = setTimeout(clear, timeout);
2964
3478
  const thr = { timeoutObject, clear, count: 0 };
2965
- action.set(path25, thr);
3479
+ action.set(path28, thr);
2966
3480
  return thr;
2967
3481
  }
2968
3482
  _incrReadyCount() {
@@ -2976,44 +3490,44 @@ var FSWatcher = class extends EventEmitter {
2976
3490
  * @param event
2977
3491
  * @param awfEmit Callback to be called when ready for event to be emitted.
2978
3492
  */
2979
- _awaitWriteFinish(path25, threshold, event, awfEmit) {
3493
+ _awaitWriteFinish(path28, threshold, event, awfEmit) {
2980
3494
  const awf = this.options.awaitWriteFinish;
2981
3495
  if (typeof awf !== "object")
2982
3496
  return;
2983
3497
  const pollInterval = awf.pollInterval;
2984
3498
  let timeoutHandler;
2985
- let fullPath = path25;
2986
- if (this.options.cwd && !sysPath2.isAbsolute(path25)) {
2987
- fullPath = sysPath2.join(this.options.cwd, path25);
3499
+ let fullPath = path28;
3500
+ if (this.options.cwd && !sysPath2.isAbsolute(path28)) {
3501
+ fullPath = sysPath2.join(this.options.cwd, path28);
2988
3502
  }
2989
3503
  const now = /* @__PURE__ */ new Date();
2990
3504
  const writes = this._pendingWrites;
2991
3505
  function awaitWriteFinishFn(prevStat) {
2992
3506
  statcb(fullPath, (err, curStat) => {
2993
- if (err || !writes.has(path25)) {
3507
+ if (err || !writes.has(path28)) {
2994
3508
  if (err && err.code !== "ENOENT")
2995
3509
  awfEmit(err);
2996
3510
  return;
2997
3511
  }
2998
3512
  const now2 = Number(/* @__PURE__ */ new Date());
2999
3513
  if (prevStat && curStat.size !== prevStat.size) {
3000
- writes.get(path25).lastChange = now2;
3514
+ writes.get(path28).lastChange = now2;
3001
3515
  }
3002
- const pw = writes.get(path25);
3516
+ const pw = writes.get(path28);
3003
3517
  const df = now2 - pw.lastChange;
3004
3518
  if (df >= threshold) {
3005
- writes.delete(path25);
3519
+ writes.delete(path28);
3006
3520
  awfEmit(void 0, curStat);
3007
3521
  } else {
3008
3522
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
3009
3523
  }
3010
3524
  });
3011
3525
  }
3012
- if (!writes.has(path25)) {
3013
- writes.set(path25, {
3526
+ if (!writes.has(path28)) {
3527
+ writes.set(path28, {
3014
3528
  lastChange: now,
3015
3529
  cancelWait: () => {
3016
- writes.delete(path25);
3530
+ writes.delete(path28);
3017
3531
  clearTimeout(timeoutHandler);
3018
3532
  return event;
3019
3533
  }
@@ -3024,8 +3538,8 @@ var FSWatcher = class extends EventEmitter {
3024
3538
  /**
3025
3539
  * Determines whether user has asked to ignore this path.
3026
3540
  */
3027
- _isIgnored(path25, stats) {
3028
- if (this.options.atomic && DOT_RE.test(path25))
3541
+ _isIgnored(path28, stats) {
3542
+ if (this.options.atomic && DOT_RE.test(path28))
3029
3543
  return true;
3030
3544
  if (!this._userIgnored) {
3031
3545
  const { cwd } = this.options;
@@ -3035,17 +3549,17 @@ var FSWatcher = class extends EventEmitter {
3035
3549
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
3036
3550
  this._userIgnored = anymatch(list, void 0);
3037
3551
  }
3038
- return this._userIgnored(path25, stats);
3552
+ return this._userIgnored(path28, stats);
3039
3553
  }
3040
- _isntIgnored(path25, stat4) {
3041
- return !this._isIgnored(path25, stat4);
3554
+ _isntIgnored(path28, stat4) {
3555
+ return !this._isIgnored(path28, stat4);
3042
3556
  }
3043
3557
  /**
3044
3558
  * Provides a set of common helpers and properties relating to symlink handling.
3045
3559
  * @param path file or directory pattern being watched
3046
3560
  */
3047
- _getWatchHelpers(path25) {
3048
- return new WatchHelper(path25, this.options.followSymlinks, this);
3561
+ _getWatchHelpers(path28) {
3562
+ return new WatchHelper(path28, this.options.followSymlinks, this);
3049
3563
  }
3050
3564
  // Directory helpers
3051
3565
  // -----------------
@@ -3077,63 +3591,63 @@ var FSWatcher = class extends EventEmitter {
3077
3591
  * @param item base path of item/directory
3078
3592
  */
3079
3593
  _remove(directory, item, isDirectory) {
3080
- const path25 = sysPath2.join(directory, item);
3081
- const fullPath = sysPath2.resolve(path25);
3082
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path25) || this._watched.has(fullPath);
3083
- if (!this._throttle("remove", path25, 100))
3594
+ const path28 = sysPath2.join(directory, item);
3595
+ const fullPath = sysPath2.resolve(path28);
3596
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path28) || this._watched.has(fullPath);
3597
+ if (!this._throttle("remove", path28, 100))
3084
3598
  return;
3085
3599
  if (!isDirectory && this._watched.size === 1) {
3086
3600
  this.add(directory, item, true);
3087
3601
  }
3088
- const wp = this._getWatchedDir(path25);
3602
+ const wp = this._getWatchedDir(path28);
3089
3603
  const nestedDirectoryChildren = wp.getChildren();
3090
- nestedDirectoryChildren.forEach((nested) => this._remove(path25, nested));
3604
+ nestedDirectoryChildren.forEach((nested) => this._remove(path28, nested));
3091
3605
  const parent = this._getWatchedDir(directory);
3092
3606
  const wasTracked = parent.has(item);
3093
3607
  parent.remove(item);
3094
3608
  if (this._symlinkPaths.has(fullPath)) {
3095
3609
  this._symlinkPaths.delete(fullPath);
3096
3610
  }
3097
- let relPath = path25;
3611
+ let relPath = path28;
3098
3612
  if (this.options.cwd)
3099
- relPath = sysPath2.relative(this.options.cwd, path25);
3613
+ relPath = sysPath2.relative(this.options.cwd, path28);
3100
3614
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
3101
3615
  const event = this._pendingWrites.get(relPath).cancelWait();
3102
3616
  if (event === EVENTS.ADD)
3103
3617
  return;
3104
3618
  }
3105
- this._watched.delete(path25);
3619
+ this._watched.delete(path28);
3106
3620
  this._watched.delete(fullPath);
3107
3621
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
3108
- if (wasTracked && !this._isIgnored(path25))
3109
- this._emit(eventName, path25);
3110
- this._closePath(path25);
3622
+ if (wasTracked && !this._isIgnored(path28))
3623
+ this._emit(eventName, path28);
3624
+ this._closePath(path28);
3111
3625
  }
3112
3626
  /**
3113
3627
  * Closes all watchers for a path
3114
3628
  */
3115
- _closePath(path25) {
3116
- this._closeFile(path25);
3117
- const dir = sysPath2.dirname(path25);
3118
- this._getWatchedDir(dir).remove(sysPath2.basename(path25));
3629
+ _closePath(path28) {
3630
+ this._closeFile(path28);
3631
+ const dir = sysPath2.dirname(path28);
3632
+ this._getWatchedDir(dir).remove(sysPath2.basename(path28));
3119
3633
  }
3120
3634
  /**
3121
3635
  * Closes only file-specific watchers
3122
3636
  */
3123
- _closeFile(path25) {
3124
- const closers = this._closers.get(path25);
3637
+ _closeFile(path28) {
3638
+ const closers = this._closers.get(path28);
3125
3639
  if (!closers)
3126
3640
  return;
3127
3641
  closers.forEach((closer) => closer());
3128
- this._closers.delete(path25);
3642
+ this._closers.delete(path28);
3129
3643
  }
3130
- _addPathCloser(path25, closer) {
3644
+ _addPathCloser(path28, closer) {
3131
3645
  if (!closer)
3132
3646
  return;
3133
- let list = this._closers.get(path25);
3647
+ let list = this._closers.get(path28);
3134
3648
  if (!list) {
3135
3649
  list = [];
3136
- this._closers.set(path25, list);
3650
+ this._closers.set(path28, list);
3137
3651
  }
3138
3652
  list.push(closer);
3139
3653
  }
@@ -3168,54 +3682,132 @@ import path14 from "path";
3168
3682
  function setupHotReload({
3169
3683
  app,
3170
3684
  appDir,
3685
+ projectRoot,
3171
3686
  route = "/__fw/hot",
3172
3687
  waitForBuild,
3173
3688
  onFileChange
3174
3689
  }) {
3175
3690
  const clients = /* @__PURE__ */ new Set();
3176
3691
  app.get(route, (req, res) => {
3177
- res.setHeader("Content-Type", "text/event-stream");
3178
- res.setHeader("Cache-Control", "no-cache");
3692
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
3693
+ res.setHeader("Cache-Control", "no-cache, no-transform");
3179
3694
  res.setHeader("Connection", "keep-alive");
3180
- res.flushHeaders?.();
3181
- res.write(`event: ping
3182
- data: connected
3183
-
3184
- `);
3695
+ res.setHeader("X-Accel-Buffering", "no");
3696
+ if (res.flushHeaders) {
3697
+ res.flushHeaders();
3698
+ } else {
3699
+ res.writeHead(200, {
3700
+ "Content-Type": "text/event-stream; charset=utf-8",
3701
+ "Cache-Control": "no-cache, no-transform",
3702
+ "Connection": "keep-alive",
3703
+ "X-Accel-Buffering": "no"
3704
+ });
3705
+ }
3185
3706
  clients.add(res);
3707
+ const pingMessage = "event: ping\ndata: connected\n\n";
3708
+ try {
3709
+ res.write(pingMessage, "utf-8");
3710
+ } catch (error) {
3711
+ console.error(`[hot-reload-server] \u274C Error sending ping:`, error);
3712
+ clients.delete(res);
3713
+ }
3186
3714
  req.on("close", () => {
3187
3715
  clients.delete(res);
3188
3716
  });
3717
+ req.on("aborted", () => {
3718
+ clients.delete(res);
3719
+ });
3189
3720
  });
3190
- const watcher = esm_default.watch(appDir, {
3721
+ console.log(`[hot-reload-server] \u2705 SSE endpoint registered at ${route}`);
3722
+ const resolvedProjectRoot = projectRoot ? path14.resolve(projectRoot) : path14.dirname(path14.resolve(appDir));
3723
+ const watcher = esm_default.watch(resolvedProjectRoot, {
3191
3724
  ignoreInitial: true,
3192
- ignored: ["**/node_modules/**", `**/${BUILD_FOLDER_NAME}/**`, "**/.git/**"]
3725
+ ignored: [
3726
+ "**/node_modules/**",
3727
+ `**/${BUILD_FOLDER_NAME}/**`,
3728
+ "**/.loly/**",
3729
+ // Ignore build output directory completely
3730
+ "**/.git/**",
3731
+ "**/dist/**",
3732
+ "**/build/**",
3733
+ "**/.next/**",
3734
+ "**/.cache/**",
3735
+ "**/.turbo/**",
3736
+ "**/.swc/**",
3737
+ "**/coverage/**",
3738
+ // Ignore generated files that trigger unnecessary reloads
3739
+ "**/*.map",
3740
+ // Source maps
3741
+ "**/*.log",
3742
+ // Log files
3743
+ "**/.DS_Store",
3744
+ // macOS
3745
+ "**/Thumbs.db"
3746
+ // Windows
3747
+ ],
3748
+ persistent: true,
3749
+ ignorePermissionErrors: true,
3750
+ // Only watch relevant source files (TypeScript, JavaScript, CSS)
3751
+ // Filter out JSON files in build directories
3752
+ awaitWriteFinish: {
3753
+ stabilityThreshold: 150,
3754
+ // Wait 150ms after file change to trigger event (debounce)
3755
+ pollInterval: 50
3756
+ // Check every 50ms
3757
+ }
3193
3758
  });
3759
+ let broadcastTimeout = null;
3760
+ const BROADCAST_DEBOUNCE_MS = 300;
3194
3761
  async function broadcastReload(reason, filePath) {
3762
+ const normalizedPath = path14.normalize(filePath);
3763
+ if (normalizedPath.includes(BUILD_FOLDER_NAME) || normalizedPath.includes(".loly") || normalizedPath.endsWith(".map") || normalizedPath.endsWith(".log") || normalizedPath.includes("route-chunks.json") || normalizedPath.includes("routes-client.ts") || normalizedPath.includes("/client/route-")) {
3764
+ return;
3765
+ }
3195
3766
  const rel = path14.relative(appDir, filePath);
3196
3767
  console.log(`[hot-reload] ${reason}: ${rel}`);
3197
- if (onFileChange) {
3198
- try {
3199
- await onFileChange(filePath);
3200
- } catch (error) {
3201
- console.warn("[hot-reload] Error in onFileChange callback:", error);
3202
- }
3768
+ if (broadcastTimeout) {
3769
+ clearTimeout(broadcastTimeout);
3203
3770
  }
3204
- if (waitForBuild) {
3205
- try {
3206
- console.log("[hot-reload] Waiting for client bundle to finish...");
3207
- await waitForBuild();
3208
- console.log("[hot-reload] Client bundle ready, sending reload event");
3209
- } catch (error) {
3210
- console.warn("[hot-reload] Error waiting for build:", error);
3771
+ broadcastTimeout = setTimeout(async () => {
3772
+ if (onFileChange) {
3773
+ try {
3774
+ await onFileChange(filePath);
3775
+ } catch (error) {
3776
+ console.warn("[hot-reload] Error in onFileChange callback:", error);
3777
+ }
3211
3778
  }
3212
- }
3213
- for (const res of clients) {
3214
- res.write(`event: message
3779
+ if (waitForBuild) {
3780
+ try {
3781
+ console.log("[hot-reload] Waiting for client bundle to finish...");
3782
+ await waitForBuild();
3783
+ console.log("[hot-reload] Client bundle ready, sending reload event");
3784
+ } catch (error) {
3785
+ console.warn("[hot-reload] Error waiting for build:", error);
3786
+ }
3787
+ }
3788
+ const message = `event: message
3215
3789
  data: reload:${rel}
3216
3790
 
3217
- `);
3218
- }
3791
+ `;
3792
+ console.log(`[hot-reload-server] \u{1F4E4} Broadcasting reload event to ${clients.size} client(s)`);
3793
+ let sentCount = 0;
3794
+ for (const res of clients) {
3795
+ try {
3796
+ if (res.writableEnded || res.destroyed) {
3797
+ clients.delete(res);
3798
+ continue;
3799
+ }
3800
+ res.write(message, "utf-8");
3801
+ sentCount++;
3802
+ } catch (error) {
3803
+ console.error(`[hot-reload-server] \u2717 Error sending to client:`, error);
3804
+ clients.delete(res);
3805
+ }
3806
+ }
3807
+ if (sentCount > 0) {
3808
+ console.log(`[hot-reload-server] \u2705 Reload event sent to ${sentCount} client(s)`);
3809
+ }
3810
+ }, BROADCAST_DEBOUNCE_MS);
3219
3811
  }
3220
3812
  watcher.on("add", (filePath) => broadcastReload("add", filePath)).on("change", (filePath) => broadcastReload("change", filePath)).on("unlink", (filePath) => broadcastReload("unlink", filePath));
3221
3813
  }
@@ -3291,6 +3883,121 @@ function deepMerge(target, source) {
3291
3883
  }
3292
3884
  return result;
3293
3885
  }
3886
+ var ConfigValidationError = class extends Error {
3887
+ constructor(message, errors = []) {
3888
+ super(message);
3889
+ this.errors = errors;
3890
+ this.name = "ConfigValidationError";
3891
+ }
3892
+ };
3893
+ function validateConfig(config, projectRoot) {
3894
+ const errors = [];
3895
+ if (!config.directories.app || typeof config.directories.app !== "string") {
3896
+ errors.push("config.directories.app must be a non-empty string");
3897
+ } else {
3898
+ const appDir = path16.join(projectRoot, config.directories.app);
3899
+ if (!fs14.existsSync(appDir) && process.env.NODE_ENV !== "test") {
3900
+ errors.push(
3901
+ `App directory not found: ${config.directories.app}
3902
+ Expected at: ${appDir}
3903
+ \u{1F4A1} Suggestion: Create the directory or update config.directories.app`
3904
+ );
3905
+ }
3906
+ }
3907
+ if (!config.directories.build || typeof config.directories.build !== "string") {
3908
+ errors.push("config.directories.build must be a non-empty string");
3909
+ }
3910
+ if (!config.directories.static || typeof config.directories.static !== "string") {
3911
+ errors.push("config.directories.static must be a non-empty string");
3912
+ }
3913
+ const conventionKeys = ["page", "layout", "notFound", "error", "api"];
3914
+ for (const key of conventionKeys) {
3915
+ if (!config.conventions[key] || typeof config.conventions[key] !== "string") {
3916
+ errors.push(`config.conventions.${key} must be a non-empty string`);
3917
+ }
3918
+ }
3919
+ if (!["always", "never", "ignore"].includes(config.routing.trailingSlash)) {
3920
+ errors.push(
3921
+ `config.routing.trailingSlash must be 'always', 'never', or 'ignore'
3922
+ Received: ${JSON.stringify(config.routing.trailingSlash)}
3923
+ \u{1F4A1} Suggestion: Use one of the valid values: 'always' | 'never' | 'ignore'`
3924
+ );
3925
+ }
3926
+ if (typeof config.routing.caseSensitive !== "boolean") {
3927
+ errors.push("config.routing.caseSensitive must be a boolean");
3928
+ }
3929
+ if (typeof config.routing.basePath !== "string") {
3930
+ errors.push("config.routing.basePath must be a string");
3931
+ } else if (config.routing.basePath && !config.routing.basePath.startsWith("/")) {
3932
+ errors.push(
3933
+ `config.routing.basePath must start with '/' (if not empty)
3934
+ Received: ${JSON.stringify(config.routing.basePath)}
3935
+ \u{1F4A1} Suggestion: Use an empty string '' or a path starting with '/', e.g., '/api'`
3936
+ );
3937
+ }
3938
+ const validClientBundlers = ["rspack", "webpack", "vite"];
3939
+ if (!validClientBundlers.includes(config.build.clientBundler)) {
3940
+ errors.push(
3941
+ `config.build.clientBundler must be one of: ${validClientBundlers.join(", ")}
3942
+ Received: ${JSON.stringify(config.build.clientBundler)}`
3943
+ );
3944
+ }
3945
+ const validServerBundlers = ["esbuild", "tsup", "swc"];
3946
+ if (!validServerBundlers.includes(config.build.serverBundler)) {
3947
+ errors.push(
3948
+ `config.build.serverBundler must be one of: ${validServerBundlers.join(", ")}
3949
+ Received: ${JSON.stringify(config.build.serverBundler)}`
3950
+ );
3951
+ }
3952
+ if (!["cjs", "esm"].includes(config.build.outputFormat)) {
3953
+ errors.push(
3954
+ `config.build.outputFormat must be 'cjs' or 'esm'
3955
+ Received: ${JSON.stringify(config.build.outputFormat)}`
3956
+ );
3957
+ }
3958
+ const validAdapters = ["express", "fastify", "koa"];
3959
+ if (!validAdapters.includes(config.server.adapter)) {
3960
+ errors.push(
3961
+ `config.server.adapter must be one of: ${validAdapters.join(", ")}
3962
+ Received: ${JSON.stringify(config.server.adapter)}`
3963
+ );
3964
+ }
3965
+ if (typeof config.server.port !== "number" || config.server.port < 1 || config.server.port > 65535) {
3966
+ errors.push(
3967
+ `config.server.port must be a number between 1 and 65535
3968
+ Received: ${JSON.stringify(config.server.port)}`
3969
+ );
3970
+ }
3971
+ if (!config.server.host || typeof config.server.host !== "string") {
3972
+ errors.push("config.server.host must be a non-empty string");
3973
+ }
3974
+ const validFrameworks = ["react", "preact", "vue", "svelte"];
3975
+ if (!validFrameworks.includes(config.rendering.framework)) {
3976
+ errors.push(
3977
+ `config.rendering.framework must be one of: ${validFrameworks.join(", ")}
3978
+ Received: ${JSON.stringify(config.rendering.framework)}`
3979
+ );
3980
+ }
3981
+ if (typeof config.rendering.streaming !== "boolean") {
3982
+ errors.push("config.rendering.streaming must be a boolean");
3983
+ }
3984
+ if (typeof config.rendering.ssr !== "boolean") {
3985
+ errors.push("config.rendering.ssr must be a boolean");
3986
+ }
3987
+ if (typeof config.rendering.ssg !== "boolean") {
3988
+ errors.push("config.rendering.ssg must be a boolean");
3989
+ }
3990
+ if (errors.length > 0) {
3991
+ const errorMessage = [
3992
+ "\u274C Configuration validation failed:",
3993
+ "",
3994
+ ...errors.map((err, i) => `${i + 1}. ${err}`),
3995
+ "",
3996
+ "\u{1F4A1} Please check your loly.config.ts file and fix the errors above."
3997
+ ].join("\n");
3998
+ throw new ConfigValidationError(errorMessage, errors);
3999
+ }
4000
+ }
3294
4001
  function loadConfig(projectRoot) {
3295
4002
  const configFiles = [
3296
4003
  path16.join(projectRoot, "loly.config.ts"),
@@ -3298,6 +4005,7 @@ function loadConfig(projectRoot) {
3298
4005
  path16.join(projectRoot, "loly.config.json")
3299
4006
  ];
3300
4007
  let userConfig = {};
4008
+ let loadedConfigFile = null;
3301
4009
  for (const configFile of configFiles) {
3302
4010
  if (fs14.existsSync(configFile)) {
3303
4011
  try {
@@ -3311,16 +4019,31 @@ function loadConfig(projectRoot) {
3311
4019
  const mod = __require(configFile);
3312
4020
  userConfig = typeof mod.default === "function" ? mod.default(process.env.NODE_ENV) : mod.default || mod.config || mod;
3313
4021
  }
4022
+ loadedConfigFile = path16.relative(projectRoot, configFile);
3314
4023
  break;
3315
4024
  } catch (error) {
3316
- console.warn(`[framework] Failed to load config from ${configFile}:`, error);
4025
+ const errorMessage = error instanceof Error ? error.message : String(error);
4026
+ throw new ConfigValidationError(
4027
+ `Failed to load configuration from ${path16.relative(projectRoot, configFile)}:
4028
+ ${errorMessage}
4029
+ \u{1F4A1} Suggestion: Check that your config file exports a valid configuration object`
4030
+ );
3317
4031
  }
3318
4032
  }
3319
4033
  }
3320
4034
  const config = deepMerge(DEFAULT_CONFIG, userConfig);
3321
- const appDir = path16.join(projectRoot, config.directories.app);
3322
- if (!fs14.existsSync(appDir) && process.env.NODE_ENV !== "test") {
3323
- console.warn(`[framework] App directory not found: ${appDir}`);
4035
+ try {
4036
+ validateConfig(config, projectRoot);
4037
+ } catch (error) {
4038
+ if (error instanceof ConfigValidationError) {
4039
+ if (loadedConfigFile) {
4040
+ error.message = `[Configuration Error in ${loadedConfigFile}]
4041
+
4042
+ ${error.message}`;
4043
+ }
4044
+ throw error;
4045
+ }
4046
+ throw error;
3324
4047
  }
3325
4048
  return config;
3326
4049
  }
@@ -3337,14 +4060,14 @@ function getStaticDir(projectRoot, config) {
3337
4060
  // modules/server/setup.ts
3338
4061
  function setupServer(app, options) {
3339
4062
  const { projectRoot, appDir, isDev, config } = options;
3340
- const routeLoader = isDev ? new FilesystemRouteLoader(appDir) : new ManifestRouteLoader(projectRoot);
4063
+ const routeLoader = isDev ? new FilesystemRouteLoader(appDir, projectRoot) : new ManifestRouteLoader(projectRoot);
3341
4064
  if (isDev) {
3342
4065
  let getRoutes2 = function() {
3343
4066
  clearAppRequireCache(appDir);
3344
- const loader = new FilesystemRouteLoader(appDir);
4067
+ sharedLoader.invalidateCache();
3345
4068
  return {
3346
- routes: loader.loadRoutes(),
3347
- apiRoutes: loader.loadApiRoutes()
4069
+ routes: sharedLoader.loadRoutes(),
4070
+ apiRoutes: sharedLoader.loadApiRoutes()
3348
4071
  };
3349
4072
  };
3350
4073
  var getRoutes = getRoutes2;
@@ -3358,19 +4081,20 @@ function setupServer(app, options) {
3358
4081
  console.log(`[hot-reload] Cleared require cache for: ${rel}`);
3359
4082
  }
3360
4083
  if (isPageFile) {
3361
- const loader = new FilesystemRouteLoader(appDir);
4084
+ const loader = new FilesystemRouteLoader(appDir, projectRoot);
3362
4085
  const newRoutes = loader.loadRoutes();
3363
4086
  writeClientRoutesManifest(newRoutes, projectRoot);
3364
4087
  console.log("[hot-reload] Client routes manifest reloaded");
3365
4088
  }
3366
4089
  };
3367
- setupHotReload({ app, appDir, waitForBuild, onFileChange });
4090
+ setupHotReload({ app, appDir, projectRoot, waitForBuild, onFileChange });
3368
4091
  app.use("/static", express.static(outDir));
3369
4092
  const routes = routeLoader.loadRoutes();
3370
4093
  const wssRoutes = routeLoader.loadWssRoutes();
3371
4094
  const notFoundPage = routeLoader.loadNotFoundRoute();
3372
4095
  const errorPage = routeLoader.loadErrorRoute();
3373
4096
  writeClientRoutesManifest(routes, projectRoot);
4097
+ const sharedLoader = new FilesystemRouteLoader(appDir, projectRoot);
3374
4098
  return {
3375
4099
  routes,
3376
4100
  wssRoutes,
@@ -3458,90 +4182,10 @@ function sanitizeQuery(query) {
3458
4182
 
3459
4183
  // modules/server/middleware/rate-limit.ts
3460
4184
  import rateLimit from "express-rate-limit";
3461
- function createRateLimiter(config = {}) {
3462
- const {
3463
- windowMs = 15 * 60 * 1e3,
3464
- // 15 minutes
3465
- max = 100,
3466
- // limit each IP to 100 requests per windowMs
3467
- message = "Too many requests from this IP, please try again later.",
3468
- standardHeaders = true,
3469
- legacyHeaders = false,
3470
- skipSuccessfulRequests = false,
3471
- skipFailedRequests = false
3472
- } = config;
3473
- return rateLimit({
3474
- windowMs,
3475
- max,
3476
- message: {
3477
- error: message
3478
- },
3479
- standardHeaders,
3480
- legacyHeaders,
3481
- skipSuccessfulRequests,
3482
- skipFailedRequests
3483
- });
3484
- }
3485
- var defaultRateLimiter = createRateLimiter({
3486
- windowMs: 15 * 60 * 1e3,
3487
- // 15 minutes
3488
- max: 100,
3489
- message: "Too many requests from this IP, please try again later."
3490
- });
3491
- var strictRateLimiter = createRateLimiter({
3492
- windowMs: 15 * 60 * 1e3,
3493
- // 15 minutes
3494
- max: 5,
3495
- message: "Too many authentication attempts, please try again later."
3496
- });
3497
- var lenientRateLimiter = createRateLimiter({
3498
- windowMs: 15 * 60 * 1e3,
3499
- // 15 minutes
3500
- max: 200,
3501
- message: "Too many requests from this IP, please try again later."
3502
- });
3503
-
3504
- // modules/server/middleware/auto-rate-limit.ts
3505
- function matchesStrictPattern(path25, patterns) {
3506
- for (const pattern of patterns) {
3507
- const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
3508
- const regex = new RegExp(`^${regexPattern}$`);
3509
- if (regex.test(path25)) {
3510
- return true;
3511
- }
3512
- }
3513
- return false;
3514
- }
3515
- function isRateLimiter(mw) {
3516
- if (!mw) return false;
3517
- if (mw === strictRateLimiter || mw === defaultRateLimiter || mw === lenientRateLimiter) {
3518
- return true;
3519
- }
3520
- if (typeof mw === "function" && mw.name && mw.name.includes("rateLimit")) {
3521
- return true;
3522
- }
3523
- if (mw && typeof mw === "function" && mw.skip || mw.resetKey) {
3524
- return true;
3525
- }
3526
- return false;
3527
- }
3528
- function getAutoRateLimiter(route, strictPatterns = []) {
3529
- const hasRateLimiter = route.middlewares?.some(isRateLimiter) || Object.values(route.methodMiddlewares || {}).some(
3530
- (mws) => mws?.some(isRateLimiter)
3531
- );
3532
- if (hasRateLimiter) {
3533
- return null;
3534
- }
3535
- if (strictPatterns.length > 0 && matchesStrictPattern(route.pattern, strictPatterns)) {
3536
- console.log(`[rate-limit] Applying strict rate limiter to route: ${route.pattern}`);
3537
- return strictRateLimiter;
3538
- }
3539
- return null;
3540
- }
3541
-
3542
- // modules/logger/index.ts
3543
- import pino from "pino";
3544
- function createLogger(options = {}) {
4185
+
4186
+ // modules/logger/index.ts
4187
+ import pino from "pino";
4188
+ function createLogger(options = {}) {
3545
4189
  const {
3546
4190
  level = process.env.LOG_LEVEL || (process.env.NODE_ENV === "development" ? "debug" : "info"),
3547
4191
  enabled = process.env.LOG_ENABLED !== "false",
@@ -3597,8 +4241,8 @@ function resetLogger() {
3597
4241
  loggerInstance = null;
3598
4242
  }
3599
4243
  var Logger = class _Logger {
3600
- constructor(logger4, context = {}) {
3601
- this.pino = logger4 || getLogger();
4244
+ constructor(logger5, context = {}) {
4245
+ this.pino = logger5 || getLogger();
3602
4246
  this.context = context;
3603
4247
  }
3604
4248
  /**
@@ -3675,12 +4319,12 @@ var DEFAULT_IGNORED_PATHS = [
3675
4319
  /^\/sockjs-node/
3676
4320
  // Hot reload websocket
3677
4321
  ];
3678
- function shouldIgnorePath(path25, ignoredPaths) {
4322
+ function shouldIgnorePath(path28, ignoredPaths) {
3679
4323
  return ignoredPaths.some((pattern) => {
3680
4324
  if (typeof pattern === "string") {
3681
- return path25 === pattern || path25.startsWith(pattern);
4325
+ return path28 === pattern || path28.startsWith(pattern);
3682
4326
  }
3683
- return pattern.test(path25);
4327
+ return pattern.test(path28);
3684
4328
  });
3685
4329
  }
3686
4330
  function requestLoggerMiddleware(options = {}) {
@@ -3731,6 +4375,151 @@ function getRequestLogger(req) {
3731
4375
  return req.logger || logger.child({ requestId: "unknown" });
3732
4376
  }
3733
4377
 
4378
+ // modules/server/middleware/rate-limit.ts
4379
+ var logger2 = createModuleLogger("rate-limit");
4380
+ function validateRateLimitConfig(config) {
4381
+ if (config.windowMs !== void 0 && (config.windowMs < 1e3 || !Number.isInteger(config.windowMs))) {
4382
+ throw new Error(
4383
+ `Invalid rateLimit.windowMs: ${config.windowMs}. Must be an integer >= 1000 (milliseconds)`
4384
+ );
4385
+ }
4386
+ if (config.max !== void 0 && (config.max < 1 || !Number.isInteger(config.max))) {
4387
+ throw new Error(
4388
+ `Invalid rateLimit.max: ${config.max}. Must be an integer >= 1`
4389
+ );
4390
+ }
4391
+ }
4392
+ function createRateLimiter(config = {}) {
4393
+ validateRateLimitConfig(config);
4394
+ const {
4395
+ windowMs = 15 * 60 * 1e3,
4396
+ // 15 minutes
4397
+ max = 100,
4398
+ // limit each IP to 100 requests per windowMs
4399
+ message = "Too many requests from this IP, please try again later.",
4400
+ standardHeaders = true,
4401
+ legacyHeaders = false,
4402
+ skipSuccessfulRequests = false,
4403
+ skipFailedRequests = false,
4404
+ keyGenerator,
4405
+ skip
4406
+ } = config;
4407
+ const limiter = rateLimit({
4408
+ windowMs,
4409
+ max,
4410
+ message: {
4411
+ error: message,
4412
+ retryAfter: Math.ceil(windowMs / 1e3)
4413
+ // seconds until retry
4414
+ },
4415
+ standardHeaders,
4416
+ legacyHeaders,
4417
+ skipSuccessfulRequests,
4418
+ skipFailedRequests,
4419
+ keyGenerator,
4420
+ skip
4421
+ });
4422
+ const wrappedLimiter = (req, res, next) => {
4423
+ limiter(req, res, (err) => {
4424
+ if (err && res.statusCode === 429) {
4425
+ const ip = req.ip || req.connection?.remoteAddress || "unknown";
4426
+ logger2.warn("Rate limit exceeded", {
4427
+ ip,
4428
+ path: req.path,
4429
+ method: req.method,
4430
+ limit: max,
4431
+ windowMs,
4432
+ retryAfter: Math.ceil(windowMs / 1e3)
4433
+ });
4434
+ }
4435
+ if (err) {
4436
+ return next(err);
4437
+ }
4438
+ next();
4439
+ });
4440
+ };
4441
+ Object.setPrototypeOf(wrappedLimiter, limiter);
4442
+ Object.assign(wrappedLimiter, limiter);
4443
+ return wrappedLimiter;
4444
+ }
4445
+ var defaultRateLimiter = createRateLimiter({
4446
+ windowMs: 15 * 60 * 1e3,
4447
+ // 15 minutes
4448
+ max: 100,
4449
+ message: "Too many requests from this IP, please try again later."
4450
+ });
4451
+ var strictRateLimiter = createRateLimiter({
4452
+ windowMs: 15 * 60 * 1e3,
4453
+ // 15 minutes
4454
+ max: 5,
4455
+ message: "Too many authentication attempts, please try again later."
4456
+ });
4457
+ var lenientRateLimiter = createRateLimiter({
4458
+ windowMs: 15 * 60 * 1e3,
4459
+ // 15 minutes
4460
+ max: 200,
4461
+ message: "Too many requests from this IP, please try again later."
4462
+ });
4463
+ function createRateLimiterFromConfig(config, useApiMax = false) {
4464
+ if (!config) return null;
4465
+ const max = useApiMax ? config.apiMax ?? config.max ?? 100 : config.max ?? 100;
4466
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
4467
+ return createRateLimiter({
4468
+ windowMs,
4469
+ max,
4470
+ message: `Too many requests from this IP, please try again after ${Math.ceil(windowMs / 1e3)} seconds.`
4471
+ });
4472
+ }
4473
+ function createStrictRateLimiterFromConfig(config) {
4474
+ if (!config || config.strictMax === void 0) {
4475
+ return strictRateLimiter;
4476
+ }
4477
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
4478
+ return createRateLimiter({
4479
+ windowMs,
4480
+ max: config.strictMax,
4481
+ message: `Too many authentication attempts, please try again after ${Math.ceil(windowMs / 1e3)} seconds.`
4482
+ });
4483
+ }
4484
+
4485
+ // modules/server/middleware/auto-rate-limit.ts
4486
+ function matchesStrictPattern(path28, patterns) {
4487
+ for (const pattern of patterns) {
4488
+ const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
4489
+ const regex = new RegExp(`^${regexPattern}$`);
4490
+ if (regex.test(path28)) {
4491
+ return true;
4492
+ }
4493
+ }
4494
+ return false;
4495
+ }
4496
+ function isRateLimiter(mw) {
4497
+ if (!mw) return false;
4498
+ if (mw === strictRateLimiter || mw === defaultRateLimiter || mw === lenientRateLimiter) {
4499
+ return true;
4500
+ }
4501
+ if (typeof mw === "function" && mw.name && mw.name.includes("rateLimit")) {
4502
+ return true;
4503
+ }
4504
+ if (mw && typeof mw === "function" && mw.skip || mw.resetKey) {
4505
+ return true;
4506
+ }
4507
+ return false;
4508
+ }
4509
+ function getAutoRateLimiter(route, strictPatterns = [], rateLimitConfig) {
4510
+ const hasRateLimiter = route.middlewares?.some(isRateLimiter) || Object.values(route.methodMiddlewares || {}).some(
4511
+ (mws) => mws?.some(isRateLimiter)
4512
+ );
4513
+ if (hasRateLimiter) {
4514
+ return null;
4515
+ }
4516
+ if (strictPatterns.length > 0 && matchesStrictPattern(route.pattern, strictPatterns)) {
4517
+ const limiter = rateLimitConfig ? createStrictRateLimiterFromConfig(rateLimitConfig) : strictRateLimiter;
4518
+ return limiter;
4519
+ }
4520
+ return null;
4521
+ }
4522
+
3734
4523
  // modules/server/handlers/api.ts
3735
4524
  async function handleApiRequest(options) {
3736
4525
  const { apiRoutes, urlPath, req, res, env = "dev" } = options;
@@ -3762,7 +4551,8 @@ async function handleApiRequest(options) {
3762
4551
  try {
3763
4552
  const autoRateLimiter = getAutoRateLimiter(
3764
4553
  route,
3765
- options.strictRateLimitPatterns
4554
+ options.strictRateLimitPatterns,
4555
+ options.rateLimitConfig
3766
4556
  );
3767
4557
  const reqLogger = getRequestLogger(req);
3768
4558
  if (autoRateLimiter) {
@@ -3774,26 +4564,46 @@ async function handleApiRequest(options) {
3774
4564
  const globalMws = route.middlewares ?? [];
3775
4565
  const perMethodMws = route.methodMiddlewares?.[method] ?? [];
3776
4566
  const chain = autoRateLimiter ? [autoRateLimiter, ...globalMws, ...perMethodMws] : [...globalMws, ...perMethodMws];
3777
- for (const mw of chain) {
3778
- const isExpressRateLimit = mw && typeof mw === "function" && (mw.skip || mw.resetKey || mw.name?.includes("rateLimit"));
3779
- if (isExpressRateLimit) {
3780
- await new Promise((resolve3, reject) => {
3781
- const next = (err) => {
3782
- if (err) reject(err);
3783
- else resolve3();
3784
- };
3785
- try {
3786
- const result = mw(req, res, next);
3787
- if (result && typeof result.then === "function") {
3788
- result.then(() => resolve3()).catch(reject);
4567
+ for (let i = 0; i < chain.length; i++) {
4568
+ const mw = chain[i];
4569
+ if (typeof mw !== "function") {
4570
+ reqLogger.warn("Invalid middleware in chain", {
4571
+ route: route.pattern,
4572
+ method,
4573
+ middlewareIndex: i,
4574
+ middlewareType: typeof mw
4575
+ });
4576
+ continue;
4577
+ }
4578
+ try {
4579
+ const isExpressRateLimit = mw.skip || mw.resetKey || mw.name?.includes("rateLimit");
4580
+ if (isExpressRateLimit) {
4581
+ await new Promise((resolve3, reject) => {
4582
+ const next = (err) => {
4583
+ if (err) reject(err);
4584
+ else resolve3();
4585
+ };
4586
+ try {
4587
+ const result = mw(req, res, next);
4588
+ if (result && typeof result.then === "function") {
4589
+ result.then(() => resolve3()).catch(reject);
4590
+ }
4591
+ } catch (err) {
4592
+ reject(err);
3789
4593
  }
3790
- } catch (err) {
3791
- reject(err);
3792
- }
4594
+ });
4595
+ } else {
4596
+ await Promise.resolve(mw(ctx, async () => {
4597
+ }));
4598
+ }
4599
+ } catch (error) {
4600
+ reqLogger.error("API middleware failed", error instanceof Error ? error : new Error(String(error)), {
4601
+ route: route.pattern,
4602
+ method,
4603
+ middlewareIndex: i,
4604
+ middlewareName: mw.name || "anonymous"
3793
4605
  });
3794
- } else {
3795
- await Promise.resolve(mw(ctx, async () => {
3796
- }));
4606
+ throw error;
3797
4607
  }
3798
4608
  if (res.headersSent) {
3799
4609
  return;
@@ -3844,34 +4654,260 @@ function createDocumentTree(options) {
3844
4654
  titleFallback,
3845
4655
  descriptionFallback,
3846
4656
  chunkHref,
4657
+ entrypointFiles = [],
3847
4658
  theme,
3848
4659
  clientJsPath = "/static/client.js",
3849
4660
  clientCssPath = "/static/client.css",
3850
- nonce
4661
+ nonce,
4662
+ includeInlineScripts = true
4663
+ // Default true - scripts inline in body for both SSR and SSG
3851
4664
  } = options;
3852
- const metaObj = meta ?? {};
3853
- const title = metaObj.title ?? titleFallback ?? "My Framework Dev";
3854
- const lang = metaObj.lang ?? "en";
3855
- const description = metaObj.description ?? descriptionFallback ?? "Demo Loly framework";
4665
+ const metaObj = meta ?? null;
4666
+ const title = metaObj?.title ?? titleFallback ?? "My Framework Dev";
4667
+ const lang = metaObj?.lang ?? "en";
4668
+ const description = metaObj?.description ?? descriptionFallback ?? "Demo Loly framework";
3856
4669
  const extraMetaTags = [];
4670
+ const linkTags = [];
3857
4671
  if (description) {
3858
4672
  extraMetaTags.push(
3859
4673
  React.createElement("meta", {
4674
+ key: "meta-description",
3860
4675
  name: "description",
3861
4676
  content: description
3862
4677
  })
3863
4678
  );
3864
4679
  }
3865
- if (Array.isArray(metaObj.metaTags)) {
3866
- for (const tag of metaObj.metaTags) {
4680
+ if (metaObj?.robots) {
4681
+ extraMetaTags.push(
4682
+ React.createElement("meta", {
4683
+ key: "meta-robots",
4684
+ name: "robots",
4685
+ content: metaObj.robots
4686
+ })
4687
+ );
4688
+ }
4689
+ if (metaObj?.themeColor) {
4690
+ extraMetaTags.push(
4691
+ React.createElement("meta", {
4692
+ key: "meta-theme-color",
4693
+ name: "theme-color",
4694
+ content: metaObj.themeColor
4695
+ })
4696
+ );
4697
+ }
4698
+ if (metaObj?.viewport) {
4699
+ extraMetaTags.push(
4700
+ React.createElement("meta", {
4701
+ key: "meta-viewport",
4702
+ name: "viewport",
4703
+ content: metaObj.viewport
4704
+ })
4705
+ );
4706
+ }
4707
+ if (metaObj?.canonical) {
4708
+ linkTags.push(
4709
+ React.createElement("link", {
4710
+ key: "link-canonical",
4711
+ rel: "canonical",
4712
+ href: metaObj.canonical
4713
+ })
4714
+ );
4715
+ }
4716
+ if (metaObj?.openGraph) {
4717
+ const og = metaObj.openGraph;
4718
+ if (og.title) {
4719
+ extraMetaTags.push(
4720
+ React.createElement("meta", {
4721
+ key: "og-title",
4722
+ property: "og:title",
4723
+ content: og.title
4724
+ })
4725
+ );
4726
+ }
4727
+ if (og.description) {
4728
+ extraMetaTags.push(
4729
+ React.createElement("meta", {
4730
+ key: "og-description",
4731
+ property: "og:description",
4732
+ content: og.description
4733
+ })
4734
+ );
4735
+ }
4736
+ if (og.type) {
4737
+ extraMetaTags.push(
4738
+ React.createElement("meta", {
4739
+ key: "og-type",
4740
+ property: "og:type",
4741
+ content: og.type
4742
+ })
4743
+ );
4744
+ }
4745
+ if (og.url) {
4746
+ extraMetaTags.push(
4747
+ React.createElement("meta", {
4748
+ key: "og-url",
4749
+ property: "og:url",
4750
+ content: og.url
4751
+ })
4752
+ );
4753
+ }
4754
+ if (og.image) {
4755
+ if (typeof og.image === "string") {
4756
+ extraMetaTags.push(
4757
+ React.createElement("meta", {
4758
+ key: "og-image",
4759
+ property: "og:image",
4760
+ content: og.image
4761
+ })
4762
+ );
4763
+ } else {
4764
+ extraMetaTags.push(
4765
+ React.createElement("meta", {
4766
+ key: "og-image",
4767
+ property: "og:image",
4768
+ content: og.image.url
4769
+ })
4770
+ );
4771
+ if (og.image.width) {
4772
+ extraMetaTags.push(
4773
+ React.createElement("meta", {
4774
+ key: "og-image-width",
4775
+ property: "og:image:width",
4776
+ content: String(og.image.width)
4777
+ })
4778
+ );
4779
+ }
4780
+ if (og.image.height) {
4781
+ extraMetaTags.push(
4782
+ React.createElement("meta", {
4783
+ key: "og-image-height",
4784
+ property: "og:image:height",
4785
+ content: String(og.image.height)
4786
+ })
4787
+ );
4788
+ }
4789
+ if (og.image.alt) {
4790
+ extraMetaTags.push(
4791
+ React.createElement("meta", {
4792
+ key: "og-image-alt",
4793
+ property: "og:image:alt",
4794
+ content: og.image.alt
4795
+ })
4796
+ );
4797
+ }
4798
+ }
4799
+ }
4800
+ if (og.siteName) {
4801
+ extraMetaTags.push(
4802
+ React.createElement("meta", {
4803
+ key: "og-site-name",
4804
+ property: "og:site_name",
4805
+ content: og.siteName
4806
+ })
4807
+ );
4808
+ }
4809
+ if (og.locale) {
4810
+ extraMetaTags.push(
4811
+ React.createElement("meta", {
4812
+ key: "og-locale",
4813
+ property: "og:locale",
4814
+ content: og.locale
4815
+ })
4816
+ );
4817
+ }
4818
+ }
4819
+ if (metaObj?.twitter) {
4820
+ const twitter = metaObj.twitter;
4821
+ if (twitter.card) {
4822
+ extraMetaTags.push(
4823
+ React.createElement("meta", {
4824
+ key: "twitter-card",
4825
+ name: "twitter:card",
4826
+ content: twitter.card
4827
+ })
4828
+ );
4829
+ }
4830
+ if (twitter.title) {
4831
+ extraMetaTags.push(
4832
+ React.createElement("meta", {
4833
+ key: "twitter-title",
4834
+ name: "twitter:title",
4835
+ content: twitter.title
4836
+ })
4837
+ );
4838
+ }
4839
+ if (twitter.description) {
4840
+ extraMetaTags.push(
4841
+ React.createElement("meta", {
4842
+ key: "twitter-description",
4843
+ name: "twitter:description",
4844
+ content: twitter.description
4845
+ })
4846
+ );
4847
+ }
4848
+ if (twitter.image) {
4849
+ extraMetaTags.push(
4850
+ React.createElement("meta", {
4851
+ key: "twitter-image",
4852
+ name: "twitter:image",
4853
+ content: twitter.image
4854
+ })
4855
+ );
4856
+ }
4857
+ if (twitter.imageAlt) {
3867
4858
  extraMetaTags.push(
3868
4859
  React.createElement("meta", {
4860
+ key: "twitter-image-alt",
4861
+ name: "twitter:image:alt",
4862
+ content: twitter.imageAlt
4863
+ })
4864
+ );
4865
+ }
4866
+ if (twitter.site) {
4867
+ extraMetaTags.push(
4868
+ React.createElement("meta", {
4869
+ key: "twitter-site",
4870
+ name: "twitter:site",
4871
+ content: twitter.site
4872
+ })
4873
+ );
4874
+ }
4875
+ if (twitter.creator) {
4876
+ extraMetaTags.push(
4877
+ React.createElement("meta", {
4878
+ key: "twitter-creator",
4879
+ name: "twitter:creator",
4880
+ content: twitter.creator
4881
+ })
4882
+ );
4883
+ }
4884
+ }
4885
+ if (metaObj?.metaTags && Array.isArray(metaObj.metaTags)) {
4886
+ metaObj.metaTags.forEach((tag, index) => {
4887
+ extraMetaTags.push(
4888
+ React.createElement("meta", {
4889
+ key: `meta-custom-${index}`,
3869
4890
  name: tag.name,
3870
4891
  property: tag.property,
4892
+ httpEquiv: tag.httpEquiv,
3871
4893
  content: tag.content
3872
4894
  })
3873
4895
  );
3874
- }
4896
+ });
4897
+ }
4898
+ if (metaObj?.links && Array.isArray(metaObj.links)) {
4899
+ metaObj.links.forEach((link, index) => {
4900
+ linkTags.push(
4901
+ React.createElement("link", {
4902
+ key: `link-custom-${index}`,
4903
+ rel: link.rel,
4904
+ href: link.href,
4905
+ as: link.as,
4906
+ crossOrigin: link.crossorigin,
4907
+ type: link.type
4908
+ })
4909
+ );
4910
+ });
3875
4911
  }
3876
4912
  const serialized = JSON.stringify({
3877
4913
  ...initialData,
@@ -3880,6 +4916,27 @@ function createDocumentTree(options) {
3880
4916
  const routerSerialized = JSON.stringify({
3881
4917
  ...routerData
3882
4918
  });
4919
+ const bodyChildren = [
4920
+ React.createElement("div", { id: APP_CONTAINER_ID }, appTree)
4921
+ ];
4922
+ if (includeInlineScripts) {
4923
+ bodyChildren.push(
4924
+ React.createElement("script", {
4925
+ key: "initial-data",
4926
+ nonce,
4927
+ dangerouslySetInnerHTML: {
4928
+ __html: `window.${WINDOW_DATA_KEY} = ${serialized};`
4929
+ }
4930
+ }),
4931
+ React.createElement("script", {
4932
+ key: "router-data",
4933
+ nonce,
4934
+ dangerouslySetInnerHTML: {
4935
+ __html: `window.${ROUTER_DATA_KEY} = ${routerSerialized};`
4936
+ }
4937
+ })
4938
+ );
4939
+ }
3883
4940
  const documentTree = React.createElement(
3884
4941
  "html",
3885
4942
  { lang },
@@ -3888,13 +4945,25 @@ function createDocumentTree(options) {
3888
4945
  null,
3889
4946
  React.createElement("meta", { charSet: "utf-8" }),
3890
4947
  React.createElement("title", null, title),
4948
+ // Viewport: use custom if provided, otherwise default
3891
4949
  React.createElement("meta", {
3892
4950
  name: "viewport",
3893
- content: "width=device-width, initial-scale=1"
4951
+ content: metaObj?.viewport ?? "width=device-width, initial-scale=1"
3894
4952
  }),
3895
4953
  ...extraMetaTags,
4954
+ ...linkTags,
4955
+ ...entrypointFiles.length > 0 ? entrypointFiles.slice(0, -1).map(
4956
+ (file) => React.createElement("link", {
4957
+ key: `preload-${file}`,
4958
+ rel: "preload",
4959
+ href: file,
4960
+ as: "script"
4961
+ })
4962
+ ) : [],
4963
+ // Preload route-specific chunk if available
3896
4964
  chunkHref && React.createElement("link", {
3897
- rel: "modulepreload",
4965
+ key: `preload-${chunkHref}`,
4966
+ rel: "preload",
3898
4967
  href: chunkHref,
3899
4968
  as: "script"
3900
4969
  }),
@@ -3906,34 +4975,35 @@ function createDocumentTree(options) {
3906
4975
  React.createElement("link", {
3907
4976
  rel: "stylesheet",
3908
4977
  href: clientCssPath
3909
- }),
3910
- React.createElement("script", {
3911
- src: clientJsPath,
3912
- defer: true
3913
- })
4978
+ }),
4979
+ ...entrypointFiles.length > 0 ? entrypointFiles.map(
4980
+ (file) => React.createElement("script", {
4981
+ key: file,
4982
+ src: file,
4983
+ defer: true,
4984
+ nonce
4985
+ // CSP nonce for external scripts
4986
+ })
4987
+ ) : [
4988
+ React.createElement("script", {
4989
+ key: "client",
4990
+ src: clientJsPath,
4991
+ defer: true,
4992
+ nonce
4993
+ // CSP nonce for external scripts
4994
+ })
4995
+ ]
3914
4996
  ),
3915
4997
  React.createElement(
3916
4998
  "body",
3917
4999
  {
3918
5000
  style: { margin: 0 },
3919
- className: [initialData.className, theme].filter(Boolean).join(" "),
5001
+ className: [initialData.className || "", theme].filter(Boolean).join(" "),
3920
5002
  suppressHydrationWarning: true
3921
5003
  // Allow theme class to differ between server and client initially
3922
5004
  },
3923
- React.createElement("div", { id: APP_CONTAINER_ID }, appTree)
3924
- ),
3925
- React.createElement("script", {
3926
- nonce,
3927
- dangerouslySetInnerHTML: {
3928
- __html: `window.${WINDOW_DATA_KEY} = ${serialized};`
3929
- }
3930
- }),
3931
- React.createElement("script", {
3932
- nonce,
3933
- dangerouslySetInnerHTML: {
3934
- __html: `window.${ROUTER_DATA_KEY} = ${routerSerialized};`
3935
- }
3936
- })
5005
+ ...bodyChildren
5006
+ )
3937
5007
  );
3938
5008
  return documentTree;
3939
5009
  }
@@ -3949,7 +5019,7 @@ function buildInitialData(urlPath, params, loaderResult) {
3949
5019
  params,
3950
5020
  props,
3951
5021
  metadata: loaderResult.metadata ?? null,
3952
- className: loaderResult.className ?? "",
5022
+ className: loaderResult.className,
3953
5023
  error: false,
3954
5024
  notFound: false
3955
5025
  };
@@ -3965,24 +5035,83 @@ var buildRouterData = (req) => {
3965
5035
  };
3966
5036
 
3967
5037
  // modules/server/handlers/middleware.ts
5038
+ import path18 from "path";
3968
5039
  async function runRouteMiddlewares(route, ctx) {
3969
- for (const mw of route.middlewares) {
3970
- await Promise.resolve(
3971
- mw(ctx, async () => {
3972
- })
3973
- );
5040
+ for (let i = 0; i < route.middlewares.length; i++) {
5041
+ const mw = route.middlewares[i];
5042
+ try {
5043
+ await Promise.resolve(
5044
+ mw(ctx, async () => {
5045
+ })
5046
+ );
5047
+ } catch (error) {
5048
+ const reqLogger = getRequestLogger(ctx.req);
5049
+ const relativePath = route.pageFile ? path18.relative(process.cwd(), route.pageFile) : route.pattern;
5050
+ reqLogger.error("Route middleware failed", error instanceof Error ? error : new Error(String(error)), {
5051
+ route: route.pattern,
5052
+ middlewareIndex: i,
5053
+ pageFile: relativePath
5054
+ });
5055
+ throw error;
5056
+ }
3974
5057
  if (ctx.res.headersSent) {
3975
5058
  return;
3976
5059
  }
3977
5060
  }
3978
5061
  }
3979
5062
 
3980
- // modules/server/handlers/loader.ts
3981
- async function runRouteLoader(route, ctx) {
5063
+ // modules/server/handlers/server-hook.ts
5064
+ import path19 from "path";
5065
+ function createServerHookErrorMessage(error, hookType, routePattern, filePath) {
5066
+ const errorMessage = error instanceof Error ? error.message : String(error);
5067
+ const errorStack = error instanceof Error ? error.stack : void 0;
5068
+ let message = `[${hookType.toUpperCase()} SERVER HOOK ERROR]
5069
+ `;
5070
+ message += `Route: ${routePattern}
5071
+ `;
5072
+ if (filePath) {
5073
+ const relativePath = path19.relative(process.cwd(), filePath);
5074
+ message += `File: ${relativePath}
5075
+ `;
5076
+ }
5077
+ message += `Error: ${errorMessage}
5078
+ `;
5079
+ if (errorMessage.includes("Cannot find module")) {
5080
+ message += `
5081
+ \u{1F4A1} Suggestion: Check that all imports in your ${hookType}.server.hook.ts are correct.
5082
+ `;
5083
+ } else if (errorMessage.includes("is not defined") || errorMessage.includes("Cannot read property")) {
5084
+ message += `
5085
+ \u{1F4A1} Suggestion: Verify that all variables and properties exist in your ${hookType}.server.hook.ts.
5086
+ `;
5087
+ } else if (errorMessage.includes("async") || errorMessage.includes("await")) {
5088
+ message += `
5089
+ \u{1F4A1} Suggestion: Make sure getServerSideProps is an async function and all promises are awaited.
5090
+ `;
5091
+ }
5092
+ if (errorStack) {
5093
+ message += `
5094
+ Stack trace:
5095
+ ${errorStack}`;
5096
+ }
5097
+ return message;
5098
+ }
5099
+ async function runRouteServerHook(route, ctx) {
3982
5100
  if (!route.loader) {
3983
5101
  return { props: {} };
3984
5102
  }
3985
- return await route.loader(ctx);
5103
+ try {
5104
+ return await route.loader(ctx);
5105
+ } catch (error) {
5106
+ const detailedError = new Error(
5107
+ createServerHookErrorMessage(error, "page", route.pattern, route.pageFile)
5108
+ );
5109
+ if (error instanceof Error && error.stack) {
5110
+ detailedError.stack = error.stack;
5111
+ }
5112
+ detailedError.originalError = error;
5113
+ throw detailedError;
5114
+ }
3986
5115
  }
3987
5116
 
3988
5117
  // modules/server/handlers/response.ts
@@ -4023,26 +5152,26 @@ function handleNotFound(res, urlPath) {
4023
5152
 
4024
5153
  // modules/server/handlers/ssg.ts
4025
5154
  import fs15 from "fs";
4026
- import path18 from "path";
4027
- var logger2 = createModuleLogger("ssg");
5155
+ import path20 from "path";
5156
+ var logger3 = createModuleLogger("ssg");
4028
5157
  function getSsgDirForPath(baseDir, urlPath) {
4029
5158
  const clean = urlPath === "/" ? "" : urlPath.replace(/^\/+/, "");
4030
- return path18.join(baseDir, clean);
5159
+ return path20.join(baseDir, clean);
4031
5160
  }
4032
5161
  function getSsgHtmlPath(baseDir, urlPath) {
4033
5162
  const dir = getSsgDirForPath(baseDir, urlPath);
4034
- return path18.join(dir, "index.html");
5163
+ return path20.join(dir, "index.html");
4035
5164
  }
4036
5165
  function getSsgDataPath(baseDir, urlPath) {
4037
5166
  const dir = getSsgDirForPath(baseDir, urlPath);
4038
- return path18.join(dir, "data.json");
5167
+ return path20.join(dir, "data.json");
4039
5168
  }
4040
5169
  function tryServeSsgHtml(res, ssgOutDir, urlPath) {
4041
5170
  const ssgHtmlPath = getSsgHtmlPath(ssgOutDir, urlPath);
4042
5171
  if (!fs15.existsSync(ssgHtmlPath)) {
4043
5172
  return false;
4044
5173
  }
4045
- logger2.info("Serving SSG HTML", { urlPath, ssgHtmlPath });
5174
+ logger3.info("Serving SSG HTML", { urlPath, ssgHtmlPath });
4046
5175
  res.setHeader(
4047
5176
  "Content-Security-Policy",
4048
5177
  "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; font-src 'self' data: https://fonts.gstatic.com; object-src 'none'; media-src 'self' https:; frame-src 'none';"
@@ -4064,13 +5193,43 @@ function tryServeSsgData(res, ssgOutDir, urlPath) {
4064
5193
  res.status(200).end(raw);
4065
5194
  return true;
4066
5195
  } catch (err) {
4067
- logger2.error("Error reading SSG data", err, { urlPath, ssgDataPath });
5196
+ logger3.error("Error reading SSG data", err, { urlPath, ssgDataPath });
4068
5197
  return false;
4069
5198
  }
4070
5199
  }
4071
5200
 
4072
5201
  // modules/server/handlers/pages.ts
4073
5202
  init_globals();
5203
+ import path21 from "path";
5204
+ function mergeMetadata(base, override) {
5205
+ if (!base && !override) return null;
5206
+ if (!base) return override;
5207
+ if (!override) return base;
5208
+ return {
5209
+ // Simple fields: override wins
5210
+ title: override.title ?? base.title,
5211
+ description: override.description ?? base.description,
5212
+ lang: override.lang ?? base.lang,
5213
+ canonical: override.canonical ?? base.canonical,
5214
+ robots: override.robots ?? base.robots,
5215
+ themeColor: override.themeColor ?? base.themeColor,
5216
+ viewport: override.viewport ?? base.viewport,
5217
+ // Nested objects: shallow merge (override wins for each field)
5218
+ openGraph: override.openGraph ? {
5219
+ ...base.openGraph,
5220
+ ...override.openGraph,
5221
+ // For image, if override has image, use it entirely (don't merge)
5222
+ image: override.openGraph.image ?? base.openGraph?.image
5223
+ } : base.openGraph,
5224
+ twitter: override.twitter ? {
5225
+ ...base.twitter,
5226
+ ...override.twitter
5227
+ } : base.twitter,
5228
+ // Arrays: override replaces base entirely (not merged)
5229
+ metaTags: override.metaTags ?? base.metaTags,
5230
+ links: override.links ?? base.links
5231
+ };
5232
+ }
4074
5233
  function isDataRequest(req) {
4075
5234
  return req.query && req.query.__fw_data === "1" || req.headers["x-fw-data"] === "1";
4076
5235
  }
@@ -4135,22 +5294,59 @@ async function handlePageRequestInternal(options) {
4135
5294
  pathname: urlPath,
4136
5295
  locals: {}
4137
5296
  };
4138
- let loaderResult2 = await runRouteLoader(notFoundPage, ctx2);
5297
+ const layoutProps2 = {};
5298
+ if (notFoundPage.layoutServerHooks && notFoundPage.layoutServerHooks.length > 0) {
5299
+ for (let i = 0; i < notFoundPage.layoutServerHooks.length; i++) {
5300
+ const layoutServerHook = notFoundPage.layoutServerHooks[i];
5301
+ if (layoutServerHook) {
5302
+ try {
5303
+ const layoutResult = await layoutServerHook(ctx2);
5304
+ if (layoutResult.props) {
5305
+ Object.assign(layoutProps2, layoutResult.props);
5306
+ }
5307
+ } catch (error) {
5308
+ const reqLogger2 = getRequestLogger(req);
5309
+ const layoutFile = notFoundPage.layoutFiles[i];
5310
+ const relativeLayoutPath = layoutFile ? path21.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5311
+ reqLogger2.warn("Layout server hook failed for not-found page", {
5312
+ error: error instanceof Error ? error.message : String(error),
5313
+ stack: error instanceof Error ? error.stack : void 0,
5314
+ layoutFile: relativeLayoutPath,
5315
+ layoutIndex: i
5316
+ });
5317
+ }
5318
+ }
5319
+ }
5320
+ }
5321
+ let loaderResult2 = await runRouteServerHook(notFoundPage, ctx2);
4139
5322
  if (!loaderResult2.theme) {
4140
5323
  loaderResult2.theme = theme;
4141
5324
  }
4142
- const initialData2 = buildInitialData(urlPath, {}, loaderResult2);
5325
+ const combinedProps2 = {
5326
+ ...layoutProps2,
5327
+ ...loaderResult2.props || {}
5328
+ };
5329
+ const combinedLoaderResult2 = {
5330
+ ...loaderResult2,
5331
+ props: combinedProps2
5332
+ };
5333
+ const initialData2 = buildInitialData(urlPath, {}, combinedLoaderResult2);
4143
5334
  const appTree2 = buildAppTree(notFoundPage, {}, initialData2.props);
4144
5335
  initialData2.notFound = true;
4145
5336
  const nonce2 = res.locals.nonce || void 0;
5337
+ const entrypointFiles2 = [];
5338
+ if (assetManifest?.entrypoints?.client) {
5339
+ entrypointFiles2.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5340
+ }
4146
5341
  const documentTree2 = createDocumentTree({
4147
5342
  appTree: appTree2,
4148
5343
  initialData: initialData2,
4149
5344
  routerData,
4150
- meta: loaderResult2.metadata ?? null,
5345
+ meta: combinedLoaderResult2.metadata ?? null,
4151
5346
  titleFallback: "Not found",
4152
5347
  descriptionFallback: "Loly demo",
4153
5348
  chunkHref: null,
5349
+ entrypointFiles: entrypointFiles2,
4154
5350
  theme,
4155
5351
  clientJsPath,
4156
5352
  clientCssPath,
@@ -4166,8 +5362,8 @@ async function handlePageRequestInternal(options) {
4166
5362
  },
4167
5363
  onShellError(err) {
4168
5364
  didError2 = true;
4169
- const reqLogger = getRequestLogger(req);
4170
- reqLogger.error("SSR shell error", err, { route: "not-found" });
5365
+ const reqLogger2 = getRequestLogger(req);
5366
+ reqLogger2.error("SSR shell error", err, { route: "not-found" });
4171
5367
  if (!res.headersSent && errorPage) {
4172
5368
  renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
4173
5369
  } else if (!res.headersSent) {
@@ -4179,8 +5375,8 @@ async function handlePageRequestInternal(options) {
4179
5375
  },
4180
5376
  onError(err) {
4181
5377
  didError2 = true;
4182
- const reqLogger = getRequestLogger(req);
4183
- reqLogger.error("SSR error", err, { route: "not-found" });
5378
+ const reqLogger2 = getRequestLogger(req);
5379
+ reqLogger2.error("SSR error", err, { route: "not-found" });
4184
5380
  }
4185
5381
  });
4186
5382
  req.on("close", () => abort2());
@@ -4202,17 +5398,62 @@ async function handlePageRequestInternal(options) {
4202
5398
  if (res.headersSent) {
4203
5399
  return;
4204
5400
  }
5401
+ const layoutProps = {};
5402
+ const layoutMetadata = [];
5403
+ const reqLogger = getRequestLogger(req);
5404
+ if (route.layoutServerHooks && route.layoutServerHooks.length > 0) {
5405
+ for (let i = 0; i < route.layoutServerHooks.length; i++) {
5406
+ const layoutServerHook = route.layoutServerHooks[i];
5407
+ if (layoutServerHook) {
5408
+ try {
5409
+ const layoutResult = await layoutServerHook(ctx);
5410
+ if (layoutResult.props) {
5411
+ Object.assign(layoutProps, layoutResult.props);
5412
+ }
5413
+ if (layoutResult.metadata) {
5414
+ layoutMetadata.push(layoutResult.metadata);
5415
+ }
5416
+ } catch (error) {
5417
+ const layoutFile = route.layoutFiles[i];
5418
+ const relativeLayoutPath = layoutFile ? path21.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5419
+ reqLogger.warn("Layout server hook failed", {
5420
+ error: error instanceof Error ? error.message : String(error),
5421
+ stack: error instanceof Error ? error.stack : void 0,
5422
+ layoutFile: relativeLayoutPath,
5423
+ route: route.pattern,
5424
+ layoutIndex: i,
5425
+ suggestion: "Check your layout.server.hook.ts file for errors"
5426
+ });
5427
+ }
5428
+ }
5429
+ }
5430
+ }
4205
5431
  let loaderResult;
4206
5432
  try {
4207
- loaderResult = await runRouteLoader(route, ctx);
5433
+ loaderResult = await runRouteServerHook(route, ctx);
4208
5434
  if (!loaderResult.theme) {
4209
5435
  loaderResult.theme = theme;
4210
5436
  }
4211
5437
  } catch (error) {
5438
+ const relativePagePath = route.pageFile ? path21.relative(projectRoot || process.cwd(), route.pageFile) : "unknown";
5439
+ reqLogger.error("Page server hook failed", {
5440
+ error: error instanceof Error ? error.message : String(error),
5441
+ stack: error instanceof Error ? error.stack : void 0,
5442
+ pageFile: relativePagePath,
5443
+ route: route.pattern,
5444
+ pathname: urlPath,
5445
+ suggestion: "Check your page.server.hook.ts (or server.hook.ts) file for errors"
5446
+ });
4212
5447
  if (isDataReq) {
4213
5448
  res.statusCode = 500;
4214
5449
  res.setHeader("Content-Type", "application/json; charset=utf-8");
4215
- res.end(JSON.stringify({ error: true, message: String(error) }));
5450
+ const errorResponse = {
5451
+ error: true,
5452
+ message: error instanceof Error ? error.message : String(error),
5453
+ route: route.pattern,
5454
+ pageFile: relativePagePath
5455
+ };
5456
+ res.end(JSON.stringify(errorResponse, null, 2));
4216
5457
  return;
4217
5458
  } else {
4218
5459
  if (errorPage) {
@@ -4223,8 +5464,28 @@ async function handlePageRequestInternal(options) {
4223
5464
  }
4224
5465
  }
4225
5466
  }
5467
+ const combinedProps = {
5468
+ ...layoutProps,
5469
+ // Props from layouts (stable)
5470
+ ...loaderResult.props || {}
5471
+ // Props from page (overrides layout)
5472
+ };
5473
+ let combinedMetadata = null;
5474
+ for (const layoutMeta of layoutMetadata) {
5475
+ if (layoutMeta) {
5476
+ combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
5477
+ }
5478
+ }
5479
+ if (loaderResult.metadata) {
5480
+ combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
5481
+ }
5482
+ const combinedLoaderResult = {
5483
+ ...loaderResult,
5484
+ props: combinedProps,
5485
+ metadata: combinedMetadata
5486
+ };
4226
5487
  if (isDataReq) {
4227
- handleDataResponse(res, loaderResult, theme);
5488
+ handleDataResponse(res, combinedLoaderResult, theme);
4228
5489
  return;
4229
5490
  }
4230
5491
  if (loaderResult.redirect) {
@@ -4239,7 +5500,7 @@ async function handlePageRequestInternal(options) {
4239
5500
  }
4240
5501
  return;
4241
5502
  }
4242
- const initialData = buildInitialData(urlPath, params, loaderResult);
5503
+ const initialData = buildInitialData(urlPath, params, combinedLoaderResult);
4243
5504
  const appTree = buildAppTree(route, params, initialData.props);
4244
5505
  const chunkName = routeChunks[route.pattern];
4245
5506
  let chunkHref = null;
@@ -4251,14 +5512,19 @@ async function handlePageRequestInternal(options) {
4251
5512
  }
4252
5513
  }
4253
5514
  const nonce = res.locals.nonce || void 0;
5515
+ const entrypointFiles = [];
5516
+ if (assetManifest?.entrypoints?.client) {
5517
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5518
+ }
4254
5519
  const documentTree = createDocumentTree({
4255
5520
  appTree,
4256
5521
  initialData,
4257
5522
  routerData,
4258
- meta: loaderResult.metadata,
5523
+ meta: combinedLoaderResult.metadata,
4259
5524
  titleFallback: "Loly framework",
4260
5525
  descriptionFallback: "Loly demo",
4261
5526
  chunkHref,
5527
+ entrypointFiles,
4262
5528
  theme,
4263
5529
  clientJsPath,
4264
5530
  clientCssPath,
@@ -4276,8 +5542,17 @@ async function handlePageRequestInternal(options) {
4276
5542
  },
4277
5543
  onShellError(err) {
4278
5544
  didError = true;
4279
- const reqLogger = getRequestLogger(req);
4280
- reqLogger.error("SSR shell error", err, { route: matched?.route?.pattern || "unknown" });
5545
+ const reqLogger2 = getRequestLogger(req);
5546
+ const routePattern = matched?.route?.pattern || "unknown";
5547
+ reqLogger2.error("SSR shell error", err, { route: routePattern });
5548
+ const errorMessage = err instanceof Error ? err.message : String(err);
5549
+ console.error(`
5550
+ \u274C [framework][ssr] Shell error for route "${routePattern}":`);
5551
+ console.error(` ${errorMessage}`);
5552
+ if (err instanceof Error && err.stack) {
5553
+ console.error(` Stack: ${err.stack.split("\n").slice(0, 3).join("\n ")}`);
5554
+ }
5555
+ console.error("\u{1F4A1} This usually indicates a React rendering error\n");
4281
5556
  if (!res.headersSent && errorPage) {
4282
5557
  renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
4283
5558
  } else if (!res.headersSent) {
@@ -4289,8 +5564,12 @@ async function handlePageRequestInternal(options) {
4289
5564
  },
4290
5565
  onError(err) {
4291
5566
  didError = true;
4292
- const reqLogger = getRequestLogger(req);
4293
- reqLogger.error("SSR error", err, { route: matched?.route?.pattern || "unknown" });
5567
+ const reqLogger2 = getRequestLogger(req);
5568
+ const routePattern = matched?.route?.pattern || "unknown";
5569
+ reqLogger2.error("SSR error", err, { route: routePattern });
5570
+ const errorMessage = err instanceof Error ? err.message : String(err);
5571
+ console.error(`\u26A0\uFE0F [framework][ssr] Error during streaming for route "${routePattern}":`);
5572
+ console.error(` ${errorMessage}`);
4294
5573
  }
4295
5574
  });
4296
5575
  req.on("close", () => {
@@ -4307,11 +5586,43 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4307
5586
  pathname: req.path,
4308
5587
  locals: { error }
4309
5588
  };
4310
- let loaderResult = await runRouteLoader(errorPage, ctx);
5589
+ const layoutProps = {};
5590
+ const reqLogger = getRequestLogger(req);
5591
+ if (errorPage.layoutServerHooks && errorPage.layoutServerHooks.length > 0) {
5592
+ for (let i = 0; i < errorPage.layoutServerHooks.length; i++) {
5593
+ const layoutServerHook = errorPage.layoutServerHooks[i];
5594
+ if (layoutServerHook) {
5595
+ try {
5596
+ const layoutResult = await layoutServerHook(ctx);
5597
+ if (layoutResult.props) {
5598
+ Object.assign(layoutProps, layoutResult.props);
5599
+ }
5600
+ } catch (err) {
5601
+ const layoutFile = errorPage.layoutFiles[i];
5602
+ const relativeLayoutPath = layoutFile ? path21.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5603
+ reqLogger.warn("Layout server hook failed for error page", {
5604
+ error: err instanceof Error ? err.message : String(err),
5605
+ stack: err instanceof Error ? err.stack : void 0,
5606
+ layoutFile: relativeLayoutPath,
5607
+ layoutIndex: i
5608
+ });
5609
+ }
5610
+ }
5611
+ }
5612
+ }
5613
+ let loaderResult = await runRouteServerHook(errorPage, ctx);
4311
5614
  if (!loaderResult.theme && theme) {
4312
5615
  loaderResult.theme = theme;
4313
5616
  }
4314
- const initialData = buildInitialData(req.path, { error: String(error) }, loaderResult);
5617
+ const combinedProps = {
5618
+ ...layoutProps,
5619
+ ...loaderResult.props || {}
5620
+ };
5621
+ const combinedLoaderResult = {
5622
+ ...loaderResult,
5623
+ props: combinedProps
5624
+ };
5625
+ const initialData = buildInitialData(req.path, { error: String(error) }, combinedLoaderResult);
4315
5626
  const routerData = buildRouterData(req);
4316
5627
  initialData.error = true;
4317
5628
  if (isDataReq) {
@@ -4321,8 +5632,8 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4321
5632
  error: true,
4322
5633
  message: String(error),
4323
5634
  props: initialData.props,
4324
- metadata: loaderResult.metadata ?? null,
4325
- theme: loaderResult.theme ?? theme ?? null
5635
+ metadata: combinedLoaderResult.metadata ?? null,
5636
+ theme: combinedLoaderResult.theme ?? theme ?? null
4326
5637
  }));
4327
5638
  return;
4328
5639
  }
@@ -4340,11 +5651,15 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4340
5651
  }
4341
5652
  }
4342
5653
  const nonce = res.locals.nonce || void 0;
5654
+ const entrypointFiles = [];
5655
+ if (assetManifest?.entrypoints?.client) {
5656
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5657
+ }
4343
5658
  const documentTree = createDocumentTree({
4344
5659
  appTree,
4345
5660
  initialData,
4346
5661
  routerData,
4347
- meta: loaderResult.metadata ?? null,
5662
+ meta: combinedLoaderResult.metadata ?? null,
4348
5663
  titleFallback: "Error",
4349
5664
  descriptionFallback: "An error occurred",
4350
5665
  chunkHref,
@@ -4365,8 +5680,8 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4365
5680
  },
4366
5681
  onShellError(err) {
4367
5682
  didError = true;
4368
- const reqLogger = getRequestLogger(req);
4369
- reqLogger.error("Error rendering error page", err, { type: "shellError" });
5683
+ const reqLogger2 = getRequestLogger(req);
5684
+ reqLogger2.error("Error rendering error page", err, { type: "shellError" });
4370
5685
  if (!res.headersSent) {
4371
5686
  res.statusCode = 500;
4372
5687
  res.setHeader("Content-Type", "text/html; charset=utf-8");
@@ -4376,8 +5691,8 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4376
5691
  },
4377
5692
  onError(err) {
4378
5693
  didError = true;
4379
- const reqLogger = getRequestLogger(req);
4380
- reqLogger.error("Error in error page", err);
5694
+ const reqLogger2 = getRequestLogger(req);
5695
+ reqLogger2.error("Error in error page", err);
4381
5696
  }
4382
5697
  });
4383
5698
  req.on("close", () => {
@@ -4465,7 +5780,7 @@ async function getServerConfig(projectRoot) {
4465
5780
  }
4466
5781
 
4467
5782
  // modules/server/routes.ts
4468
- import path19 from "path";
5783
+ import path22 from "path";
4469
5784
  function setupRoutes(options) {
4470
5785
  const {
4471
5786
  app,
@@ -4480,21 +5795,23 @@ function setupRoutes(options) {
4480
5795
  config
4481
5796
  } = options;
4482
5797
  const routeChunks = routeLoader.loadRouteChunks();
4483
- const ssgOutDir = path19.join(
4484
- config ? getBuildDir(projectRoot, config) : path19.join(projectRoot, BUILD_FOLDER_NAME),
5798
+ const ssgOutDir = path22.join(
5799
+ config ? getBuildDir(projectRoot, config) : path22.join(projectRoot, BUILD_FOLDER_NAME),
4485
5800
  "ssg"
4486
5801
  );
4487
5802
  app.all("/api/*", async (req, res) => {
4488
5803
  const apiRoutes = isDev && getRoutes ? getRoutes().apiRoutes : initialApiRoutes;
4489
5804
  const serverConfig = await getServerConfig(projectRoot);
4490
5805
  const strictPatterns = serverConfig.rateLimit?.strictPatterns || [];
5806
+ const rateLimitConfig = serverConfig.rateLimit;
4491
5807
  await handleApiRequest({
4492
5808
  apiRoutes,
4493
5809
  urlPath: req.path,
4494
5810
  req,
4495
5811
  res,
4496
5812
  env: isDev ? "dev" : "prod",
4497
- strictRateLimitPatterns: strictPatterns
5813
+ strictRateLimitPatterns: strictPatterns,
5814
+ rateLimitConfig
4498
5815
  });
4499
5816
  });
4500
5817
  app.get("*", async (req, res) => {
@@ -4747,12 +6064,29 @@ var setupApplication = async ({
4747
6064
  corsOptions.origin = process.env.NODE_ENV === "development";
4748
6065
  }
4749
6066
  app.use(cors(corsOptions));
4750
- if (rateLimit2 && process.env.NODE_ENV !== "development") {
4751
- const generalLimiter = createRateLimiter({
4752
- windowMs: rateLimit2.windowMs,
4753
- max: rateLimit2.max
4754
- });
4755
- app.use(generalLimiter);
6067
+ if (rateLimit2) {
6068
+ const shouldApply = process.env.NODE_ENV !== "development" || process.env.ENABLE_RATE_LIMIT === "true";
6069
+ if (shouldApply) {
6070
+ try {
6071
+ const generalLimiter = createRateLimiterFromConfig(rateLimit2, false);
6072
+ if (generalLimiter) {
6073
+ app.use(generalLimiter);
6074
+ const logger5 = createModuleLogger("server");
6075
+ logger5.info("Rate limiting enabled", {
6076
+ windowMs: rateLimit2.windowMs ?? 15 * 60 * 1e3,
6077
+ max: rateLimit2.max ?? 100,
6078
+ apiMax: rateLimit2.apiMax,
6079
+ strictMax: rateLimit2.strictMax,
6080
+ strictPatterns: rateLimit2.strictPatterns?.length ?? 0
6081
+ });
6082
+ }
6083
+ } catch (error) {
6084
+ const logger5 = createModuleLogger("server");
6085
+ logger5.error("Failed to setup rate limiting", {
6086
+ error: error instanceof Error ? error.message : String(error)
6087
+ });
6088
+ }
6089
+ }
4756
6090
  }
4757
6091
  app.use(cookieParser());
4758
6092
  app.use(express2.json({ limit: bodyLimit }));
@@ -4767,22 +6101,31 @@ var setupApplication = async ({
4767
6101
 
4768
6102
  // src/server.ts
4769
6103
  import dotenv2 from "dotenv";
4770
- var envPath = path20.join(process.cwd(), ".env");
6104
+ var envPath = path23.join(process.cwd(), ".env");
4771
6105
  if (fs16.existsSync(envPath)) {
4772
6106
  dotenv2.config({ path: envPath });
4773
6107
  } else {
4774
6108
  dotenv2.config();
4775
6109
  }
4776
- var logger3 = createModuleLogger("server");
6110
+ var logger4 = createModuleLogger("server");
4777
6111
  async function startServer(options = {}) {
4778
6112
  const isDev = options.isDev ?? process.env.NODE_ENV === "development";
4779
6113
  const projectRoot = options.rootDir ?? process.cwd();
4780
- const config = options.config ?? loadConfig(projectRoot);
6114
+ let config;
6115
+ try {
6116
+ config = options.config ?? loadConfig(projectRoot);
6117
+ } catch (error) {
6118
+ if (error instanceof ConfigValidationError) {
6119
+ console.error("\n" + error.message + "\n");
6120
+ process.exit(1);
6121
+ }
6122
+ throw error;
6123
+ }
4781
6124
  const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? config.server.port;
4782
6125
  const host = process.env.HOST ?? (!isDev ? "0.0.0.0" : void 0) ?? config.server.host;
4783
- const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : path20.join(getBuildDir(projectRoot, config), "server"));
6126
+ const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : path23.join(getBuildDir(projectRoot, config), "server"));
4784
6127
  if (!isDev && !fs16.existsSync(appDir)) {
4785
- logger3.error("Compiled directory not found", void 0, {
6128
+ logger4.error("Compiled directory not found", void 0, {
4786
6129
  buildDir: config.directories.build,
4787
6130
  appDir,
4788
6131
  environment: "production"
@@ -4799,7 +6142,7 @@ async function startServer(options = {}) {
4799
6142
  isDev,
4800
6143
  config
4801
6144
  });
4802
- const routeLoader = isDev ? new FilesystemRouteLoader(appDir) : new ManifestRouteLoader(projectRoot);
6145
+ const routeLoader = isDev ? new FilesystemRouteLoader(appDir, projectRoot) : new ManifestRouteLoader(projectRoot);
4803
6146
  setupWssEvents({
4804
6147
  httpServer,
4805
6148
  wssRoutes
@@ -4851,10 +6194,10 @@ async function startProdServer(options = {}) {
4851
6194
  }
4852
6195
 
4853
6196
  // modules/build/ssg/builder.ts
4854
- import path23 from "path";
6197
+ import path26 from "path";
4855
6198
 
4856
6199
  // modules/build/ssg/path.ts
4857
- import path21 from "path";
6200
+ import path24 from "path";
4858
6201
  function buildPathFromPattern(pattern, params) {
4859
6202
  const segments = pattern.split("/").filter(Boolean);
4860
6203
  const parts = [];
@@ -4883,12 +6226,12 @@ function buildPathFromPattern(pattern, params) {
4883
6226
  }
4884
6227
  function pathToOutDir(baseDir, urlPath) {
4885
6228
  const clean = urlPath === "/" ? "" : urlPath.replace(/^\/+/, "");
4886
- return path21.join(baseDir, clean);
6229
+ return path24.join(baseDir, clean);
4887
6230
  }
4888
6231
 
4889
6232
  // modules/build/ssg/renderer.ts
4890
6233
  import fs17 from "fs";
4891
- import path22 from "path";
6234
+ import path25 from "path";
4892
6235
  import { renderToString } from "react-dom/server";
4893
6236
  init_globals();
4894
6237
  async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params) {
@@ -4905,6 +6248,10 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
4905
6248
  chunkHref = `${STATIC_PATH}/${chunkName}.js`;
4906
6249
  }
4907
6250
  }
6251
+ const entrypointFiles = [];
6252
+ if (assetManifest?.entrypoints?.client) {
6253
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
6254
+ }
4908
6255
  const req = {
4909
6256
  method: "GET",
4910
6257
  headers: {},
@@ -4934,32 +6281,79 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
4934
6281
  })
4935
6282
  );
4936
6283
  }
6284
+ const layoutProps = {};
6285
+ const layoutMetadata = [];
6286
+ if (route.layoutServerHooks && route.layoutServerHooks.length > 0) {
6287
+ for (let i = 0; i < route.layoutServerHooks.length; i++) {
6288
+ const layoutServerHook = route.layoutServerHooks[i];
6289
+ if (layoutServerHook) {
6290
+ try {
6291
+ const layoutResult = await layoutServerHook(ctx);
6292
+ if (layoutResult.props) {
6293
+ Object.assign(layoutProps, layoutResult.props);
6294
+ }
6295
+ if (layoutResult.metadata) {
6296
+ layoutMetadata.push(layoutResult.metadata);
6297
+ }
6298
+ } catch (error) {
6299
+ console.warn(
6300
+ `\u26A0\uFE0F [framework][ssg] Layout server hook ${i} failed for route ${route.pattern}:`,
6301
+ error instanceof Error ? error.message : String(error)
6302
+ );
6303
+ if (error instanceof Error && error.stack) {
6304
+ console.warn(` Stack: ${error.stack.split("\n").slice(0, 3).join("\n ")}`);
6305
+ }
6306
+ }
6307
+ }
6308
+ }
6309
+ }
4937
6310
  let loaderResult = { props: {} };
4938
6311
  if (route.loader) {
4939
6312
  loaderResult = await route.loader(ctx);
4940
6313
  }
4941
- if (loaderResult.redirect || loaderResult.notFound) {
6314
+ const combinedProps = {
6315
+ ...layoutProps,
6316
+ ...loaderResult.props || {}
6317
+ };
6318
+ let combinedMetadata = null;
6319
+ for (const layoutMeta of layoutMetadata) {
6320
+ if (layoutMeta) {
6321
+ combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
6322
+ }
6323
+ }
6324
+ if (loaderResult.metadata) {
6325
+ combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
6326
+ }
6327
+ const combinedLoaderResult = {
6328
+ ...loaderResult,
6329
+ props: combinedProps,
6330
+ metadata: combinedMetadata
6331
+ };
6332
+ if (combinedLoaderResult.redirect || combinedLoaderResult.notFound) {
4942
6333
  return;
4943
6334
  }
4944
- const initialData = buildInitialData(urlPath, params, loaderResult);
6335
+ const initialData = buildInitialData(urlPath, params, combinedLoaderResult);
4945
6336
  const routerData = buildRouterData(req);
4946
6337
  const appTree = buildAppTree(route, params, initialData.props);
4947
6338
  const documentTree = createDocumentTree({
4948
6339
  appTree,
4949
6340
  initialData,
4950
6341
  routerData,
4951
- meta: loaderResult.metadata,
6342
+ meta: combinedLoaderResult.metadata,
4952
6343
  titleFallback: "My Framework Dev",
4953
6344
  descriptionFallback: "Static page generated by @lolyjs/core.",
4954
6345
  chunkHref,
6346
+ entrypointFiles,
4955
6347
  clientJsPath,
4956
- clientCssPath
6348
+ clientCssPath,
6349
+ includeInlineScripts: true
6350
+ // SSG needs inline scripts (renderToString doesn't support bootstrapScripts)
4957
6351
  });
4958
6352
  const html = "<!DOCTYPE html>" + renderToString(documentTree);
4959
6353
  const dir = pathToOutDir(ssgOutDir, urlPath);
4960
6354
  ensureDir(dir);
4961
- const htmlFile = path22.join(dir, "index.html");
4962
- const dataFile = path22.join(dir, "data.json");
6355
+ const htmlFile = path25.join(dir, "index.html");
6356
+ const dataFile = path25.join(dir, "data.json");
4963
6357
  fs17.writeFileSync(htmlFile, html, "utf-8");
4964
6358
  fs17.writeFileSync(dataFile, JSON.stringify(initialData, null, 2), "utf-8");
4965
6359
  }
@@ -4967,7 +6361,7 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
4967
6361
  // modules/build/ssg/builder.ts
4968
6362
  init_globals();
4969
6363
  async function buildStaticPages(projectRoot, routes) {
4970
- const ssgOutDir = path23.join(projectRoot, BUILD_FOLDER_NAME, "ssg");
6364
+ const ssgOutDir = path26.join(projectRoot, BUILD_FOLDER_NAME, "ssg");
4971
6365
  ensureDir(ssgOutDir);
4972
6366
  for (const route of routes) {
4973
6367
  if (route.dynamic !== "force-static") continue;
@@ -4977,12 +6371,15 @@ async function buildStaticPages(projectRoot, routes) {
4977
6371
  } else {
4978
6372
  if (!route.generateStaticParams) {
4979
6373
  console.warn(
4980
- `[framework][ssg] Route ${route.pattern} is marked as force-static but has no generateStaticParams function. Skipping.`
6374
+ `\u26A0\uFE0F [framework][ssg] Route "${route.pattern}" is marked as force-static but has no generateStaticParams function`
4981
6375
  );
6376
+ console.warn(` \u{1F4A1} Add a generateStaticParams export to enable static generation for this route`);
6377
+ console.warn(` Skipping this route...
6378
+ `);
4982
6379
  continue;
4983
6380
  }
4984
6381
  try {
4985
- console.log(`[framework][ssg] Generating static params for route: ${route.pattern}`);
6382
+ console.log(`\u{1F4E6} [framework][ssg] Generating static params for route: ${route.pattern}`);
4986
6383
  let timeoutId = null;
4987
6384
  const timeoutPromise = new Promise((_, reject) => {
4988
6385
  timeoutId = setTimeout(() => {
@@ -4997,12 +6394,16 @@ async function buildStaticPages(projectRoot, routes) {
4997
6394
  clearTimeout(timeoutId);
4998
6395
  }
4999
6396
  allParams = sp;
5000
- console.log(`[framework][ssg] Generated ${sp.length} static params for route: ${route.pattern}`);
6397
+ console.log(` \u2705 Generated ${sp.length} static params for route: ${route.pattern}`);
5001
6398
  } catch (error) {
5002
- console.error(
5003
- `[framework][ssg] Error generating static params for route ${route.pattern}:`,
5004
- error
5005
- );
6399
+ console.error(`
6400
+ \u274C [framework][ssg] Error generating static params for route "${route.pattern}":`);
6401
+ console.error(error instanceof Error ? error.message : String(error));
6402
+ if (error instanceof Error && error.stack) {
6403
+ console.error(` Stack: ${error.stack.split("\n").slice(0, 3).join("\n ")}`);
6404
+ }
6405
+ console.error(`\u{1F4A1} Check your generateStaticParams function for this route
6406
+ `);
5006
6407
  throw error;
5007
6408
  }
5008
6409
  }
@@ -5011,11 +6412,11 @@ async function buildStaticPages(projectRoot, routes) {
5011
6412
  await renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params);
5012
6413
  }
5013
6414
  }
5014
- console.log(`[framework][ssg] Finished building all static pages`);
6415
+ console.log(`\u2705 [framework][ssg] Finished building all static pages`);
5015
6416
  }
5016
6417
 
5017
6418
  // modules/build/bundler/server.ts
5018
- import path24 from "path";
6419
+ import path27 from "path";
5019
6420
  import fs18 from "fs";
5020
6421
  import esbuild from "esbuild";
5021
6422
  init_globals();
@@ -5025,7 +6426,7 @@ function collectAppSources(appDir) {
5025
6426
  function walk(dir) {
5026
6427
  const items = fs18.readdirSync(dir, { withFileTypes: true });
5027
6428
  for (const item of items) {
5028
- const full = path24.join(dir, item.name);
6429
+ const full = path27.join(dir, item.name);
5029
6430
  if (item.isDirectory()) {
5030
6431
  walk(full);
5031
6432
  continue;
@@ -5042,7 +6443,7 @@ function collectAppSources(appDir) {
5042
6443
  return entries;
5043
6444
  }
5044
6445
  async function buildServerApp(projectRoot, appDir) {
5045
- const outDir = path24.join(projectRoot, BUILD_FOLDER_NAME, "server");
6446
+ const outDir = path27.join(projectRoot, BUILD_FOLDER_NAME, "server");
5046
6447
  const entryPoints = collectAppSources(appDir);
5047
6448
  ensureDir(outDir);
5048
6449
  if (entryPoints.length === 0) {
@@ -5060,12 +6461,12 @@ async function buildServerApp(projectRoot, appDir) {
5060
6461
  bundle: true,
5061
6462
  splitting: false,
5062
6463
  logLevel: "info",
5063
- tsconfig: path24.join(projectRoot, "tsconfig.json"),
6464
+ tsconfig: path27.join(projectRoot, "tsconfig.json"),
5064
6465
  packages: "external"
5065
6466
  });
5066
6467
  for (const fileName of SERVER_FILES) {
5067
- const initTS = path24.join(projectRoot, `${fileName}.ts`);
5068
- const initJS = path24.join(outDir, `${fileName}.js`);
6468
+ const initTS = path27.join(projectRoot, `${fileName}.ts`);
6469
+ const initJS = path27.join(outDir, `${fileName}.js`);
5069
6470
  if (fs18.existsSync(initTS)) {
5070
6471
  await esbuild.build({
5071
6472
  entryPoints: [initTS],
@@ -5077,7 +6478,7 @@ async function buildServerApp(projectRoot, appDir) {
5077
6478
  sourcemap: true,
5078
6479
  bundle: false,
5079
6480
  logLevel: "info",
5080
- tsconfig: path24.join(projectRoot, "tsconfig.json")
6481
+ tsconfig: path27.join(projectRoot, "tsconfig.json")
5081
6482
  });
5082
6483
  }
5083
6484
  }
@@ -5220,22 +6621,158 @@ function matchRouteClient(pathWithSearch, routes) {
5220
6621
  }
5221
6622
 
5222
6623
  // modules/runtime/client/metadata.ts
6624
+ function getOrCreateMeta(selector, attributes) {
6625
+ let meta = document.querySelector(selector);
6626
+ if (!meta) {
6627
+ meta = document.createElement("meta");
6628
+ if (attributes.name) meta.name = attributes.name;
6629
+ if (attributes.property) meta.setAttribute("property", attributes.property);
6630
+ if (attributes.httpEquiv) meta.httpEquiv = attributes.httpEquiv;
6631
+ document.head.appendChild(meta);
6632
+ }
6633
+ return meta;
6634
+ }
6635
+ function getOrCreateLink(rel, href) {
6636
+ const selector = `link[rel="${rel}"]`;
6637
+ let link = document.querySelector(selector);
6638
+ if (!link) {
6639
+ link = document.createElement("link");
6640
+ link.rel = rel;
6641
+ link.href = href;
6642
+ document.head.appendChild(link);
6643
+ } else {
6644
+ link.href = href;
6645
+ }
6646
+ return link;
6647
+ }
5223
6648
  function applyMetadata(md) {
5224
6649
  if (!md) return;
5225
6650
  if (md.title) {
5226
6651
  document.title = md.title;
5227
6652
  }
5228
6653
  if (md.description) {
5229
- let meta = document.querySelector(
5230
- 'meta[name="description"]'
5231
- );
5232
- if (!meta) {
5233
- meta = document.createElement("meta");
5234
- meta.name = "description";
5235
- document.head.appendChild(meta);
5236
- }
6654
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
5237
6655
  meta.content = md.description;
5238
6656
  }
6657
+ if (md.robots) {
6658
+ const meta = getOrCreateMeta('meta[name="robots"]', { name: "robots" });
6659
+ meta.content = md.robots;
6660
+ }
6661
+ if (md.themeColor) {
6662
+ const meta = getOrCreateMeta('meta[name="theme-color"]', { name: "theme-color" });
6663
+ meta.content = md.themeColor;
6664
+ }
6665
+ if (md.viewport) {
6666
+ const meta = getOrCreateMeta('meta[name="viewport"]', { name: "viewport" });
6667
+ meta.content = md.viewport;
6668
+ }
6669
+ if (md.canonical) {
6670
+ getOrCreateLink("canonical", md.canonical);
6671
+ }
6672
+ if (md.openGraph) {
6673
+ const og = md.openGraph;
6674
+ if (og.title) {
6675
+ const meta = getOrCreateMeta('meta[property="og:title"]', { property: "og:title" });
6676
+ meta.content = og.title;
6677
+ }
6678
+ if (og.description) {
6679
+ const meta = getOrCreateMeta('meta[property="og:description"]', { property: "og:description" });
6680
+ meta.content = og.description;
6681
+ }
6682
+ if (og.type) {
6683
+ const meta = getOrCreateMeta('meta[property="og:type"]', { property: "og:type" });
6684
+ meta.content = og.type;
6685
+ }
6686
+ if (og.url) {
6687
+ const meta = getOrCreateMeta('meta[property="og:url"]', { property: "og:url" });
6688
+ meta.content = og.url;
6689
+ }
6690
+ if (og.image) {
6691
+ if (typeof og.image === "string") {
6692
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
6693
+ meta.content = og.image;
6694
+ } else {
6695
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
6696
+ meta.content = og.image.url;
6697
+ if (og.image.width) {
6698
+ const metaWidth = getOrCreateMeta('meta[property="og:image:width"]', { property: "og:image:width" });
6699
+ metaWidth.content = String(og.image.width);
6700
+ }
6701
+ if (og.image.height) {
6702
+ const metaHeight = getOrCreateMeta('meta[property="og:image:height"]', { property: "og:image:height" });
6703
+ metaHeight.content = String(og.image.height);
6704
+ }
6705
+ if (og.image.alt) {
6706
+ const metaAlt = getOrCreateMeta('meta[property="og:image:alt"]', { property: "og:image:alt" });
6707
+ metaAlt.content = og.image.alt;
6708
+ }
6709
+ }
6710
+ }
6711
+ if (og.siteName) {
6712
+ const meta = getOrCreateMeta('meta[property="og:site_name"]', { property: "og:site_name" });
6713
+ meta.content = og.siteName;
6714
+ }
6715
+ if (og.locale) {
6716
+ const meta = getOrCreateMeta('meta[property="og:locale"]', { property: "og:locale" });
6717
+ meta.content = og.locale;
6718
+ }
6719
+ }
6720
+ if (md.twitter) {
6721
+ const twitter = md.twitter;
6722
+ if (twitter.card) {
6723
+ const meta = getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" });
6724
+ meta.content = twitter.card;
6725
+ }
6726
+ if (twitter.title) {
6727
+ const meta = getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" });
6728
+ meta.content = twitter.title;
6729
+ }
6730
+ if (twitter.description) {
6731
+ const meta = getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" });
6732
+ meta.content = twitter.description;
6733
+ }
6734
+ if (twitter.image) {
6735
+ const meta = getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" });
6736
+ meta.content = twitter.image;
6737
+ }
6738
+ if (twitter.imageAlt) {
6739
+ const meta = getOrCreateMeta('meta[name="twitter:image:alt"]', { name: "twitter:image:alt" });
6740
+ meta.content = twitter.imageAlt;
6741
+ }
6742
+ if (twitter.site) {
6743
+ const meta = getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" });
6744
+ meta.content = twitter.site;
6745
+ }
6746
+ if (twitter.creator) {
6747
+ const meta = getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" });
6748
+ meta.content = twitter.creator;
6749
+ }
6750
+ }
6751
+ if (md.metaTags && Array.isArray(md.metaTags)) {
6752
+ md.metaTags.forEach((tag) => {
6753
+ let selector = "";
6754
+ if (tag.name) {
6755
+ selector = `meta[name="${tag.name}"]`;
6756
+ } else if (tag.property) {
6757
+ selector = `meta[property="${tag.property}"]`;
6758
+ } else if (tag.httpEquiv) {
6759
+ selector = `meta[http-equiv="${tag.httpEquiv}"]`;
6760
+ }
6761
+ if (selector) {
6762
+ const meta = getOrCreateMeta(selector, {
6763
+ name: tag.name,
6764
+ property: tag.property,
6765
+ httpEquiv: tag.httpEquiv
6766
+ });
6767
+ meta.content = tag.content;
6768
+ }
6769
+ });
6770
+ }
6771
+ if (md.links && Array.isArray(md.links)) {
6772
+ md.links.forEach((link) => {
6773
+ getOrCreateLink(link.rel, link.href);
6774
+ });
6775
+ }
5239
6776
  }
5240
6777
 
5241
6778
  // modules/runtime/client/AppShell.tsx
@@ -5452,10 +6989,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
5452
6989
  });
5453
6990
  return true;
5454
6991
  } catch (loadError) {
5455
- console.error(
5456
- "[client] Error loading error route components:",
5457
- loadError
5458
- );
6992
+ console.error("\n\u274C [client] Error loading error route components:");
6993
+ console.error(loadError);
6994
+ if (loadError instanceof Error) {
6995
+ console.error(` Message: ${loadError.message}`);
6996
+ if (loadError.stack) {
6997
+ console.error(` Stack: ${loadError.stack.split("\n").slice(0, 3).join("\n ")}`);
6998
+ }
6999
+ }
7000
+ console.error("\u{1F4A1} Falling back to full page reload\n");
5459
7001
  window.location.href = nextUrl;
5460
7002
  return false;
5461
7003
  }
@@ -5560,7 +7102,11 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
5560
7102
  searchParams: Object.fromEntries(url.searchParams.entries())
5561
7103
  };
5562
7104
  setRouterData(routerData);
5563
- const components = await matched.route.load();
7105
+ const prefetched = prefetchedRoutes.get(matched.route);
7106
+ const components = prefetched ? await prefetched : await matched.route.load();
7107
+ if (!prefetched) {
7108
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
7109
+ }
5564
7110
  window.scrollTo({
5565
7111
  top: 0,
5566
7112
  behavior: "smooth"
@@ -5599,7 +7145,7 @@ async function navigate(nextUrl, handlers, options) {
5599
7145
  }
5600
7146
  }
5601
7147
  if (!ok) {
5602
- if (json && json.redirect) {
7148
+ if (json?.redirect) {
5603
7149
  window.location.href = json.redirect.destination;
5604
7150
  return;
5605
7151
  }
@@ -5620,6 +7166,47 @@ async function navigate(nextUrl, handlers, options) {
5620
7166
  window.location.href = nextUrl;
5621
7167
  }
5622
7168
  }
7169
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
7170
+ function prefetchRoute(url, routes, notFoundRoute) {
7171
+ const [pathname] = url.split("?");
7172
+ const matched = matchRouteClient(pathname, routes);
7173
+ if (!matched) {
7174
+ if (notFoundRoute) {
7175
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
7176
+ if (!existing2) {
7177
+ const promise = notFoundRoute.load();
7178
+ prefetchedRoutes.set(notFoundRoute, promise);
7179
+ }
7180
+ }
7181
+ return;
7182
+ }
7183
+ const existing = prefetchedRoutes.get(matched.route);
7184
+ if (!existing) {
7185
+ const promise = matched.route.load();
7186
+ prefetchedRoutes.set(matched.route, promise);
7187
+ }
7188
+ }
7189
+ function createHoverHandler(routes, notFoundRoute) {
7190
+ return function handleHover(ev) {
7191
+ try {
7192
+ const target = ev.target;
7193
+ if (!target) return;
7194
+ const anchor = target.closest("a[href]");
7195
+ if (!anchor) return;
7196
+ const href = anchor.getAttribute("href");
7197
+ if (!href) return;
7198
+ if (href.startsWith("#")) return;
7199
+ const url = new URL(href, window.location.href);
7200
+ if (url.origin !== window.location.origin) return;
7201
+ if (anchor.target && anchor.target !== "_self") return;
7202
+ const nextUrl = url.pathname + url.search;
7203
+ const currentUrl = window.location.pathname + window.location.search;
7204
+ if (nextUrl === currentUrl) return;
7205
+ prefetchRoute(nextUrl, routes, notFoundRoute);
7206
+ } catch (error) {
7207
+ }
7208
+ };
7209
+ }
5623
7210
  function createClickHandler(navigate2) {
5624
7211
  return function handleClick(ev) {
5625
7212
  try {
@@ -5730,17 +7317,20 @@ function AppShell({
5730
7317
  }
5731
7318
  const handleClick = createClickHandler(handleNavigateInternal);
5732
7319
  const handlePopState = createPopStateHandler(handleNavigateInternal);
7320
+ const handleHover = createHoverHandler(routes, notFoundRoute);
5733
7321
  window.addEventListener("click", handleClick, false);
5734
7322
  window.addEventListener("popstate", handlePopState, false);
7323
+ window.addEventListener("mouseover", handleHover, false);
5735
7324
  return () => {
5736
7325
  isMounted = false;
5737
7326
  window.removeEventListener("click", handleClick, false);
5738
7327
  window.removeEventListener("popstate", handlePopState, false);
7328
+ window.removeEventListener("mouseover", handleHover, false);
5739
7329
  };
5740
- }, []);
7330
+ }, [routes, notFoundRoute]);
5741
7331
  useEffect(() => {
5742
7332
  const handleDataRefresh = () => {
5743
- const freshData = window?.__FW_DATA__;
7333
+ const freshData = window[WINDOW_DATA_KEY2];
5744
7334
  if (!freshData) return;
5745
7335
  const currentPathname = window.location.pathname;
5746
7336
  const freshPathname = freshData.pathname;
@@ -5767,6 +7357,91 @@ function AppShell({
5767
7357
  return /* @__PURE__ */ jsx2(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ jsx2(RouterView, { state }, routeKey) });
5768
7358
  }
5769
7359
 
7360
+ // modules/runtime/client/hot-reload.ts
7361
+ function setupHotReload2() {
7362
+ const nodeEnv = process.env.NODE_ENV || "production";
7363
+ const isDev = nodeEnv === "development";
7364
+ console.log(`[hot-reload] NODE_ENV: ${nodeEnv}, isDev: ${isDev}`);
7365
+ if (!isDev) {
7366
+ console.log("[hot-reload] Skipping hot reload setup (not in development mode)");
7367
+ return;
7368
+ }
7369
+ console.log("[hot-reload] Setting up hot reload client...");
7370
+ let eventSource = null;
7371
+ let reloadTimeout = null;
7372
+ let reconnectTimeout = null;
7373
+ let reconnectAttempts = 0;
7374
+ const MAX_RECONNECT_ATTEMPTS = 10;
7375
+ const RECONNECT_DELAY = 1e3;
7376
+ const RELOAD_DELAY = 100;
7377
+ function connect() {
7378
+ try {
7379
+ if (eventSource) {
7380
+ console.log("[hot-reload] Closing existing EventSource connection");
7381
+ eventSource.close();
7382
+ }
7383
+ const endpoint = "/__fw/hot";
7384
+ eventSource = new EventSource(endpoint);
7385
+ eventSource.addEventListener("ping", (event) => {
7386
+ if ("data" in event) {
7387
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
7388
+ }
7389
+ reconnectAttempts = 0;
7390
+ });
7391
+ eventSource.addEventListener("message", (event) => {
7392
+ const data = event.data;
7393
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
7394
+ const filePath = data.slice(7);
7395
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
7396
+ if (reloadTimeout) {
7397
+ clearTimeout(reloadTimeout);
7398
+ }
7399
+ reloadTimeout = setTimeout(() => {
7400
+ try {
7401
+ window.location.reload();
7402
+ } catch (error) {
7403
+ console.error("[hot-reload] \u274C Error reloading page:", error);
7404
+ setTimeout(() => window.location.reload(), 100);
7405
+ }
7406
+ }, RELOAD_DELAY);
7407
+ }
7408
+ });
7409
+ eventSource.onopen = () => {
7410
+ reconnectAttempts = 0;
7411
+ };
7412
+ eventSource.onerror = (error) => {
7413
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
7414
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
7415
+ if (eventSource?.readyState === EventSource.CONNECTING) {
7416
+ console.log("[hot-reload] \u23F3 Still connecting...");
7417
+ return;
7418
+ } else if (eventSource?.readyState === EventSource.OPEN) {
7419
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
7420
+ } else {
7421
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
7422
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
7423
+ reconnectAttempts++;
7424
+ const delay = RECONNECT_DELAY * reconnectAttempts;
7425
+ if (reconnectTimeout) {
7426
+ clearTimeout(reconnectTimeout);
7427
+ }
7428
+ reconnectTimeout = setTimeout(() => {
7429
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
7430
+ connect();
7431
+ }, delay);
7432
+ } else {
7433
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
7434
+ }
7435
+ }
7436
+ };
7437
+ } catch (error) {
7438
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
7439
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
7440
+ }
7441
+ }
7442
+ connect();
7443
+ }
7444
+
5770
7445
  // modules/runtime/client/bootstrap.tsx
5771
7446
  import { jsx as jsx3 } from "react/jsx-runtime";
5772
7447
  async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute, errorRoute) {
@@ -5808,101 +7483,91 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
5808
7483
  props: initialData?.props ?? {}
5809
7484
  };
5810
7485
  }
5811
- function setupHotReload2() {
5812
- const nodeEnv = typeof process !== "undefined" && process.env?.NODE_ENV || "production";
5813
- const isDev = nodeEnv !== "production";
5814
- if (!isDev) {
5815
- return;
7486
+ function initializeRouterData(initialUrl, initialData) {
7487
+ let routerData = getRouterData();
7488
+ if (!routerData) {
7489
+ const url = new URL(initialUrl, window.location.origin);
7490
+ routerData = {
7491
+ pathname: url.pathname,
7492
+ params: initialData?.params || {},
7493
+ searchParams: Object.fromEntries(url.searchParams.entries())
7494
+ };
7495
+ setRouterData(routerData);
5816
7496
  }
7497
+ }
7498
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
5817
7499
  try {
5818
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
5819
- const eventSource = new EventSource("/__fw/hot");
5820
- let reloadTimeout = null;
5821
- eventSource.addEventListener("message", (event) => {
5822
- const data = event.data;
5823
- if (data && data.startsWith("reload:")) {
5824
- const filePath = data.slice(7);
5825
- console.log(`[hot-reload] File changed: ${filePath}`);
5826
- if (reloadTimeout) {
5827
- clearTimeout(reloadTimeout);
5828
- }
5829
- reloadTimeout = setTimeout(() => {
5830
- console.log("[hot-reload] Reloading page...");
5831
- window.location.reload();
5832
- }, 500);
5833
- }
5834
- });
5835
- eventSource.addEventListener("ping", () => {
5836
- console.log("[hot-reload] \u2713 Connected to hot reload server");
5837
- });
5838
- eventSource.onopen = () => {
5839
- console.log("[hot-reload] \u2713 SSE connection opened");
5840
- };
5841
- eventSource.onerror = (error) => {
5842
- const states = ["CONNECTING", "OPEN", "CLOSED"];
5843
- const state = states[eventSource.readyState] || "UNKNOWN";
5844
- if (eventSource.readyState === EventSource.CONNECTING) {
5845
- console.log("[hot-reload] Connecting...");
5846
- } else if (eventSource.readyState === EventSource.OPEN) {
5847
- console.warn("[hot-reload] Connection error (but connection is open):", error);
5848
- } else {
5849
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
7500
+ const initialState = await loadInitialRoute(
7501
+ initialUrl,
7502
+ initialData,
7503
+ routes,
7504
+ notFoundRoute,
7505
+ errorRoute
7506
+ );
7507
+ if (initialData?.metadata) {
7508
+ try {
7509
+ applyMetadata(initialData.metadata);
7510
+ } catch (metadataError) {
7511
+ console.warn("[client] Error applying metadata:", metadataError);
5850
7512
  }
5851
- };
7513
+ }
7514
+ hydrateRoot(
7515
+ container,
7516
+ /* @__PURE__ */ jsx3(
7517
+ AppShell,
7518
+ {
7519
+ initialState,
7520
+ routes,
7521
+ notFoundRoute,
7522
+ errorRoute
7523
+ }
7524
+ )
7525
+ );
5852
7526
  } catch (error) {
5853
- console.log("[hot-reload] EventSource not supported or error:", error);
7527
+ console.error(
7528
+ "[client] Error loading initial route components for",
7529
+ initialUrl,
7530
+ error
7531
+ );
7532
+ throw error;
5854
7533
  }
5855
7534
  }
5856
7535
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
5857
- console.log("[client] Bootstrap starting, setting up hot reload...");
5858
7536
  setupHotReload2();
5859
- (async function bootstrap() {
5860
- const container = document.getElementById(APP_CONTAINER_ID2);
5861
- const initialData = getWindowData();
5862
- if (!container) {
5863
- console.error(`Container #${APP_CONTAINER_ID2} not found for hydration`);
5864
- return;
5865
- }
5866
- const initialUrl = window.location.pathname + window.location.search;
5867
- let routerData = getRouterData();
5868
- if (!routerData) {
5869
- const url = new URL(initialUrl, window.location.origin);
5870
- routerData = {
5871
- pathname: url.pathname,
5872
- params: initialData?.params || {},
5873
- searchParams: Object.fromEntries(url.searchParams.entries())
5874
- };
5875
- setRouterData(routerData);
5876
- }
7537
+ (async () => {
5877
7538
  try {
5878
- const initialState = await loadInitialRoute(
7539
+ const container = document.getElementById(APP_CONTAINER_ID2);
7540
+ if (!container) {
7541
+ console.error(`
7542
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID2} not found`);
7543
+ console.error("\u{1F4A1} This usually means:");
7544
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
7545
+ console.error(" \u2022 The container was removed before hydration");
7546
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
7547
+ return;
7548
+ }
7549
+ const initialData = getWindowData();
7550
+ const initialUrl = window.location.pathname + window.location.search;
7551
+ initializeRouterData(initialUrl, initialData);
7552
+ await hydrateInitialRoute(
7553
+ container,
5879
7554
  initialUrl,
5880
7555
  initialData,
5881
7556
  routes,
5882
7557
  notFoundRoute,
5883
7558
  errorRoute
5884
7559
  );
5885
- if (initialData?.metadata) {
5886
- applyMetadata(initialData.metadata);
5887
- }
5888
- hydrateRoot(
5889
- container,
5890
- /* @__PURE__ */ jsx3(
5891
- AppShell,
5892
- {
5893
- initialState,
5894
- routes,
5895
- notFoundRoute,
5896
- errorRoute
5897
- }
5898
- )
5899
- );
5900
7560
  } catch (error) {
5901
- console.error(
5902
- "[client] Error loading initial route components for",
5903
- initialUrl,
5904
- error
5905
- );
7561
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
7562
+ console.error(error);
7563
+ if (error instanceof Error) {
7564
+ console.error("\nError details:");
7565
+ console.error(` Message: ${error.message}`);
7566
+ if (error.stack) {
7567
+ console.error(` Stack: ${error.stack}`);
7568
+ }
7569
+ }
7570
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
5906
7571
  window.location.reload();
5907
7572
  }
5908
7573
  })();
@@ -5930,11 +7595,11 @@ var ValidationError = class extends Error {
5930
7595
  format() {
5931
7596
  const formatted = {};
5932
7597
  for (const error of this.errors) {
5933
- const path25 = error.path.join(".");
5934
- if (!formatted[path25]) {
5935
- formatted[path25] = [];
7598
+ const path28 = error.path.join(".");
7599
+ if (!formatted[path28]) {
7600
+ formatted[path28] = [];
5936
7601
  }
5937
- formatted[path25].push(error.message);
7602
+ formatted[path28].push(error.message);
5938
7603
  }
5939
7604
  return formatted;
5940
7605
  }