@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.cjs CHANGED
@@ -116,7 +116,7 @@ module.exports = __toCommonJS(src_exports);
116
116
 
117
117
  // src/server.ts
118
118
  var import_fs18 = __toESM(require("fs"));
119
- var import_path22 = __toESM(require("path"));
119
+ var import_path25 = __toESM(require("path"));
120
120
 
121
121
  // modules/server/utils/server-dir.ts
122
122
  var import_fs = __toESM(require("fs"));
@@ -246,7 +246,7 @@ function loadLayoutsForDir(pageDir, appDir) {
246
246
  };
247
247
  }
248
248
 
249
- // modules/router/loader.ts
249
+ // modules/router/server-hook.ts
250
250
  var import_fs3 = __toESM(require("fs"));
251
251
  var import_path3 = __toESM(require("path"));
252
252
  var NAMING = {
@@ -258,14 +258,16 @@ var NAMING = {
258
258
  // Files
259
259
  SERVER_HOOK: "server.hook"
260
260
  };
261
- function loadLoaderForDir(currentDir) {
262
- const loaderTs = import_path3.default.join(currentDir, `${NAMING.SERVER_HOOK}.ts`);
263
- const loaderJs = import_path3.default.join(currentDir, `${NAMING.SERVER_HOOK}.js`);
264
- const file = import_fs3.default.existsSync(loaderTs) ? loaderTs : import_fs3.default.existsSync(loaderJs) ? loaderJs : null;
261
+ function loadServerHookForDir(currentDir) {
262
+ const pageServerHookTs = import_path3.default.join(currentDir, `page.server.hook.ts`);
263
+ const pageServerHookJs = import_path3.default.join(currentDir, `page.server.hook.js`);
264
+ const serverHookTs = import_path3.default.join(currentDir, `${NAMING.SERVER_HOOK}.ts`);
265
+ const serverHookJs = import_path3.default.join(currentDir, `${NAMING.SERVER_HOOK}.js`);
266
+ const file = import_fs3.default.existsSync(pageServerHookTs) ? pageServerHookTs : import_fs3.default.existsSync(pageServerHookJs) ? pageServerHookJs : import_fs3.default.existsSync(serverHookTs) ? serverHookTs : import_fs3.default.existsSync(serverHookJs) ? serverHookJs : null;
265
267
  if (!file) {
266
268
  return {
267
269
  middlewares: [],
268
- loader: null,
270
+ serverHook: null,
269
271
  dynamic: "auto",
270
272
  generateStaticParams: null
271
273
  };
@@ -281,31 +283,119 @@ function loadLoaderForDir(currentDir) {
281
283
  mod = require(file);
282
284
  } catch (error) {
283
285
  console.error(
284
- `[framework][loader] Error loading server hook from ${file}:`,
286
+ `[framework][server-hook] Error loading server hook from ${file}:`,
285
287
  error
286
288
  );
287
289
  return {
288
290
  middlewares: [],
289
- loader: null,
291
+ serverHook: null,
290
292
  dynamic: "auto",
291
293
  generateStaticParams: null
292
294
  };
293
295
  }
294
- const middlewares = Array.isArray(
295
- mod?.[NAMING.BEFORE_MIDDLEWARES]
296
- ) ? mod[NAMING.BEFORE_MIDDLEWARES] : [];
297
- const loader = typeof mod?.[NAMING.GET_SERVER_DATA_FN] === "function" ? mod[NAMING.GET_SERVER_DATA_FN] : null;
296
+ let middlewares = [];
297
+ const rawMiddlewares = mod?.[NAMING.BEFORE_MIDDLEWARES];
298
+ if (rawMiddlewares !== void 0) {
299
+ if (!Array.isArray(rawMiddlewares)) {
300
+ console.warn(
301
+ `[framework][server-hook] ${NAMING.BEFORE_MIDDLEWARES} must be an array in ${file}, ignoring invalid value`
302
+ );
303
+ } else {
304
+ for (let i = 0; i < rawMiddlewares.length; i++) {
305
+ const mw = rawMiddlewares[i];
306
+ if (typeof mw !== "function") {
307
+ console.warn(
308
+ `[framework][server-hook] Middleware at index ${i} in ${NAMING.BEFORE_MIDDLEWARES} is not a function in ${file}, skipping`
309
+ );
310
+ continue;
311
+ }
312
+ middlewares.push(mw);
313
+ }
314
+ }
315
+ }
316
+ const serverHook = typeof mod?.[NAMING.GET_SERVER_DATA_FN] === "function" ? mod[NAMING.GET_SERVER_DATA_FN] : null;
298
317
  const dynamic = mod?.[NAMING.RENDER_TYPE_CONST] === "force-static" || mod?.[NAMING.RENDER_TYPE_CONST] === "force-dynamic" ? mod.dynamic : "auto";
299
318
  const generateStaticParams = typeof mod?.[NAMING.GENERATE_SSG_PARAMS] === "function" ? mod[NAMING.GENERATE_SSG_PARAMS] : null;
300
319
  return {
301
320
  middlewares,
302
- loader,
321
+ serverHook,
303
322
  dynamic,
304
323
  generateStaticParams
305
324
  };
306
325
  }
326
+ function loadLayoutServerHook(layoutFile) {
327
+ const layoutDir = import_path3.default.dirname(layoutFile);
328
+ const layoutBasename = import_path3.default.basename(layoutFile, import_path3.default.extname(layoutFile));
329
+ const serverHookTs = import_path3.default.join(layoutDir, `${layoutBasename}.server.hook.ts`);
330
+ const serverHookJs = import_path3.default.join(layoutDir, `${layoutBasename}.server.hook.js`);
331
+ const file = import_fs3.default.existsSync(serverHookTs) ? serverHookTs : import_fs3.default.existsSync(serverHookJs) ? serverHookJs : null;
332
+ if (!file) {
333
+ return null;
334
+ }
335
+ if (file.endsWith(".ts") || file.endsWith(".tsx")) {
336
+ try {
337
+ require("tsx/cjs");
338
+ } catch (e) {
339
+ }
340
+ }
341
+ try {
342
+ const mod = require(file);
343
+ const serverHook = typeof mod?.getServerSideProps === "function" ? mod.getServerSideProps : null;
344
+ return serverHook;
345
+ } catch (error) {
346
+ console.error(
347
+ `[framework][server-hook] Error loading layout server hook from ${file}:`,
348
+ error
349
+ );
350
+ return null;
351
+ }
352
+ }
307
353
 
308
354
  // modules/router/loader-pages.ts
355
+ function validateRoutes(routes, appDir) {
356
+ const routePatterns = /* @__PURE__ */ new Map();
357
+ const errors = [];
358
+ const warnings = [];
359
+ for (const route of routes) {
360
+ const existing = routePatterns.get(route.pattern) || [];
361
+ existing.push(route);
362
+ routePatterns.set(route.pattern, existing);
363
+ }
364
+ for (const [pattern, duplicateRoutes] of routePatterns.entries()) {
365
+ if (duplicateRoutes.length > 1) {
366
+ const files = duplicateRoutes.map(
367
+ (r) => r.pageFile ? import_path4.default.relative(appDir, r.pageFile) : "unknown"
368
+ ).join(", ");
369
+ errors.push(
370
+ `Duplicate route pattern "${pattern}" found in multiple files:
371
+ ${files}
372
+ \u{1F4A1} Suggestion: Ensure each route has a unique path pattern`
373
+ );
374
+ }
375
+ }
376
+ for (const route of routes) {
377
+ if (!route.pageFile || !import_fs4.default.existsSync(route.pageFile)) {
378
+ warnings.push(
379
+ `Route pattern "${route.pattern}" references a missing page file`
380
+ );
381
+ }
382
+ }
383
+ if (errors.length > 0) {
384
+ const errorMessage = [
385
+ "\u274C Route validation failed:",
386
+ "",
387
+ ...errors,
388
+ "",
389
+ "\u{1F4A1} Please fix the errors above before starting the server."
390
+ ].join("\n");
391
+ throw new Error(errorMessage);
392
+ }
393
+ if (warnings.length > 0 && process.env.NODE_ENV === "development") {
394
+ console.warn("\n\u26A0\uFE0F Route warnings:");
395
+ warnings.forEach((warning) => console.warn(` \u2022 ${warning}`));
396
+ console.warn("");
397
+ }
398
+ }
309
399
  function loadRoutes(appDir) {
310
400
  if (!import_fs4.default.existsSync(appDir)) {
311
401
  return [];
@@ -335,7 +425,12 @@ function loadRoutes(appDir) {
335
425
  currentDir,
336
426
  appDir
337
427
  );
338
- const { middlewares, loader, dynamic, generateStaticParams } = loadLoaderForDir(currentDir);
428
+ const layoutServerHooks = [];
429
+ for (const layoutFile of layoutFiles) {
430
+ const layoutServerHook = loadLayoutServerHook(layoutFile);
431
+ layoutServerHooks.push(layoutServerHook);
432
+ }
433
+ const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(currentDir);
339
434
  routes.push({
340
435
  pattern: routePath,
341
436
  regex,
@@ -345,13 +440,17 @@ function loadRoutes(appDir) {
345
440
  pageFile: fullPath,
346
441
  layoutFiles,
347
442
  middlewares,
348
- loader,
443
+ loader: serverHook,
444
+ // Keep 'loader' field name for backward compatibility
445
+ layoutServerHooks,
446
+ // Server hooks for each layout (same order as layouts)
349
447
  dynamic,
350
448
  generateStaticParams
351
449
  });
352
450
  }
353
451
  }
354
452
  walk(appDir);
453
+ validateRoutes(routes, appDir);
355
454
  return routes;
356
455
  }
357
456
 
@@ -715,9 +814,12 @@ function writeClientBoostrapManifest(projectRoot) {
715
814
  lines.push("");
716
815
  lines.push(`import { bootstrapClient } from "@lolyjs/core/runtime"`);
717
816
  lines.push("");
718
- lines.push(
719
- "bootstrapClient(routes as ClientRouteLoaded[], notFoundRoute, errorRoute);"
720
- );
817
+ lines.push(`try {`);
818
+ lines.push(` bootstrapClient(routes as ClientRouteLoaded[], notFoundRoute, errorRoute);`);
819
+ lines.push(`} catch (error) {`);
820
+ lines.push(` console.error("[bootstrap] Fatal error during bootstrap:", error);`);
821
+ lines.push(` throw error;`);
822
+ lines.push(`}`);
721
823
  import_fs7.default.writeFileSync(manifestPath, lines.join("\n"), "utf-8");
722
824
  }
723
825
  function writeRoutesManifest({
@@ -864,7 +966,12 @@ function loadRoutesFromManifest(projectRoot) {
864
966
  (f) => import_path10.default.join(projectRoot, f)
865
967
  );
866
968
  const pageDir = import_path10.default.dirname(pageFile);
867
- const { middlewares, loader, dynamic, generateStaticParams } = loadLoaderForDir(pageDir);
969
+ const layoutServerHooks = [];
970
+ for (const layoutFile of layoutFiles) {
971
+ const layoutServerHook = loadLayoutServerHook(layoutFile);
972
+ layoutServerHooks.push(layoutServerHook);
973
+ }
974
+ const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(pageDir);
868
975
  pageRoutes.push({
869
976
  pattern: entry.pattern,
870
977
  regex,
@@ -874,7 +981,10 @@ function loadRoutesFromManifest(projectRoot) {
874
981
  pageFile,
875
982
  layoutFiles,
876
983
  middlewares,
877
- loader,
984
+ loader: serverHook,
985
+ // Keep 'loader' field name for backward compatibility
986
+ layoutServerHooks,
987
+ // Server hooks for each layout (same order as layouts)
878
988
  dynamic: entry.dynamic ?? dynamic,
879
989
  generateStaticParams
880
990
  });
@@ -1047,24 +1157,210 @@ function loadWssRoutes(appDir) {
1047
1157
  }
1048
1158
 
1049
1159
  // modules/router/route-loader.ts
1160
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1161
+ "node_modules",
1162
+ ".git",
1163
+ ".loly",
1164
+ "dist",
1165
+ "build",
1166
+ ".next",
1167
+ ".cache",
1168
+ "coverage",
1169
+ ".vscode",
1170
+ ".idea",
1171
+ ".turbo",
1172
+ ".swc"
1173
+ ]);
1174
+ function getRelevantFiles(appDir, projectRoot) {
1175
+ const files = [];
1176
+ const projectRootNormalized = import_path12.default.resolve(projectRoot);
1177
+ function walk(currentDir) {
1178
+ if (!import_fs10.default.existsSync(currentDir)) {
1179
+ return;
1180
+ }
1181
+ const entries = import_fs10.default.readdirSync(currentDir, { withFileTypes: true });
1182
+ for (const entry of entries) {
1183
+ const fullPath = import_path12.default.join(currentDir, entry.name);
1184
+ if (entry.isDirectory()) {
1185
+ if (SKIP_DIRECTORIES.has(entry.name)) {
1186
+ continue;
1187
+ }
1188
+ if (entry.name.startsWith(".")) {
1189
+ continue;
1190
+ }
1191
+ walk(fullPath);
1192
+ } else {
1193
+ const ext = import_path12.default.extname(entry.name);
1194
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
1195
+ files.push(fullPath);
1196
+ }
1197
+ }
1198
+ }
1199
+ }
1200
+ walk(projectRootNormalized);
1201
+ return files;
1202
+ }
1203
+ function hasFilesChanged(appDir, projectRoot, cachedStats) {
1204
+ const currentFiles = getRelevantFiles(appDir, projectRoot);
1205
+ const currentFilesSet = new Set(currentFiles);
1206
+ for (const [filePath] of cachedStats.entries()) {
1207
+ if (!currentFilesSet.has(filePath)) {
1208
+ return true;
1209
+ }
1210
+ }
1211
+ for (const filePath of currentFiles) {
1212
+ if (!import_fs10.default.existsSync(filePath)) {
1213
+ continue;
1214
+ }
1215
+ const stats = import_fs10.default.statSync(filePath);
1216
+ const cachedStat = cachedStats.get(filePath);
1217
+ if (!cachedStat) {
1218
+ return true;
1219
+ }
1220
+ if (stats.mtimeMs !== cachedStat.mtime || stats.size !== cachedStat.size) {
1221
+ return true;
1222
+ }
1223
+ }
1224
+ return false;
1225
+ }
1226
+ function buildFileStats(files) {
1227
+ const statsMap = /* @__PURE__ */ new Map();
1228
+ for (const filePath of files) {
1229
+ if (import_fs10.default.existsSync(filePath)) {
1230
+ const stats = import_fs10.default.statSync(filePath);
1231
+ statsMap.set(filePath, {
1232
+ mtime: stats.mtimeMs,
1233
+ size: stats.size
1234
+ });
1235
+ }
1236
+ }
1237
+ return statsMap;
1238
+ }
1050
1239
  var FilesystemRouteLoader = class {
1051
- constructor(appDir) {
1240
+ // Maximum cache age in ms (1 second fallback)
1241
+ constructor(appDir, projectRoot = appDir) {
1052
1242
  this.appDir = appDir;
1243
+ this.projectRoot = projectRoot;
1244
+ this.cache = null;
1245
+ this.cacheMaxAge = 1e3;
1246
+ if (this.projectRoot === this.appDir) {
1247
+ let current = import_path12.default.resolve(this.appDir);
1248
+ while (current !== import_path12.default.dirname(current)) {
1249
+ if (import_fs10.default.existsSync(import_path12.default.join(current, "package.json"))) {
1250
+ this.projectRoot = current;
1251
+ break;
1252
+ }
1253
+ current = import_path12.default.dirname(current);
1254
+ }
1255
+ }
1256
+ }
1257
+ /**
1258
+ * Invalidates the cache, forcing a reload on next access.
1259
+ */
1260
+ invalidateCache() {
1261
+ this.cache = null;
1262
+ }
1263
+ /**
1264
+ * Checks if cache is still valid, invalidates if files changed.
1265
+ */
1266
+ ensureCacheValid() {
1267
+ if (!this.cache) {
1268
+ return;
1269
+ }
1270
+ const now = Date.now();
1271
+ if (now - this.cache.timestamp > this.cacheMaxAge) {
1272
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats)) {
1273
+ this.cache = null;
1274
+ } else {
1275
+ this.cache.timestamp = now;
1276
+ }
1277
+ }
1053
1278
  }
1054
1279
  loadRoutes() {
1055
- return loadRoutes(this.appDir);
1280
+ this.ensureCacheValid();
1281
+ if (!this.cache || hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats)) {
1282
+ const routes = loadRoutes(this.appDir);
1283
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1284
+ const fileStats = buildFileStats(files);
1285
+ this.cache = {
1286
+ routes,
1287
+ apiRoutes: this.cache?.apiRoutes || [],
1288
+ wssRoutes: this.cache?.wssRoutes || [],
1289
+ notFoundRoute: this.cache?.notFoundRoute ?? null,
1290
+ errorRoute: this.cache?.errorRoute ?? null,
1291
+ fileStats,
1292
+ timestamp: Date.now()
1293
+ };
1294
+ }
1295
+ return this.cache.routes;
1056
1296
  }
1057
1297
  loadApiRoutes() {
1058
- return loadApiRoutes(this.appDir);
1298
+ this.ensureCacheValid();
1299
+ if (!this.cache) {
1300
+ this.loadRoutes();
1301
+ }
1302
+ if (!this.cache) {
1303
+ throw new Error("Failed to initialize route cache");
1304
+ }
1305
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.apiRoutes.length === 0) {
1306
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1307
+ const fileStats = buildFileStats(files);
1308
+ this.cache.apiRoutes = loadApiRoutes(this.appDir);
1309
+ this.cache.fileStats = fileStats;
1310
+ this.cache.timestamp = Date.now();
1311
+ }
1312
+ return this.cache.apiRoutes;
1059
1313
  }
1060
1314
  loadWssRoutes() {
1061
- return loadWssRoutes(this.appDir);
1315
+ this.ensureCacheValid();
1316
+ if (!this.cache) {
1317
+ this.loadRoutes();
1318
+ }
1319
+ if (!this.cache) {
1320
+ throw new Error("Failed to initialize route cache");
1321
+ }
1322
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.wssRoutes.length === 0) {
1323
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1324
+ const fileStats = buildFileStats(files);
1325
+ this.cache.wssRoutes = loadWssRoutes(this.appDir);
1326
+ this.cache.fileStats = fileStats;
1327
+ this.cache.timestamp = Date.now();
1328
+ }
1329
+ return this.cache.wssRoutes;
1062
1330
  }
1063
1331
  loadNotFoundRoute() {
1064
- return loadNotFoundRouteFromFilesystem(this.appDir);
1332
+ this.ensureCacheValid();
1333
+ if (!this.cache) {
1334
+ this.loadRoutes();
1335
+ }
1336
+ if (!this.cache) {
1337
+ throw new Error("Failed to initialize route cache");
1338
+ }
1339
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.notFoundRoute === void 0) {
1340
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1341
+ const fileStats = buildFileStats(files);
1342
+ this.cache.notFoundRoute = loadNotFoundRouteFromFilesystem(this.appDir);
1343
+ this.cache.fileStats = fileStats;
1344
+ this.cache.timestamp = Date.now();
1345
+ }
1346
+ return this.cache.notFoundRoute;
1065
1347
  }
1066
1348
  loadErrorRoute() {
1067
- return loadErrorRouteFromFilesystem(this.appDir);
1349
+ this.ensureCacheValid();
1350
+ if (!this.cache) {
1351
+ this.loadRoutes();
1352
+ }
1353
+ if (!this.cache) {
1354
+ throw new Error("Failed to initialize route cache");
1355
+ }
1356
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.errorRoute === void 0) {
1357
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1358
+ const fileStats = buildFileStats(files);
1359
+ this.cache.errorRoute = loadErrorRouteFromFilesystem(this.appDir);
1360
+ this.cache.fileStats = fileStats;
1361
+ this.cache.timestamp = Date.now();
1362
+ }
1363
+ return this.cache.errorRoute;
1068
1364
  }
1069
1365
  loadRouteChunks() {
1070
1366
  return {};
@@ -1073,27 +1369,67 @@ var FilesystemRouteLoader = class {
1073
1369
  var ManifestRouteLoader = class {
1074
1370
  constructor(projectRoot) {
1075
1371
  this.projectRoot = projectRoot;
1372
+ this.cache = {};
1373
+ }
1374
+ /**
1375
+ * Gets the manifest, using cache if available.
1376
+ * The manifest is read once and cached for the lifetime of the loader.
1377
+ */
1378
+ getManifest() {
1379
+ if (this.cache.manifest !== void 0) {
1380
+ return this.cache.manifest;
1381
+ }
1382
+ const manifest = readManifest(this.projectRoot);
1383
+ this.cache.manifest = manifest;
1384
+ return manifest;
1076
1385
  }
1077
1386
  loadRoutes() {
1387
+ if (this.cache.routes) {
1388
+ return this.cache.routes;
1389
+ }
1078
1390
  const { routes } = loadRoutesFromManifest(this.projectRoot);
1391
+ this.cache.routes = routes;
1079
1392
  return routes;
1080
1393
  }
1081
1394
  loadApiRoutes() {
1395
+ if (this.cache.apiRoutes) {
1396
+ return this.cache.apiRoutes;
1397
+ }
1082
1398
  const { apiRoutes } = loadRoutesFromManifest(this.projectRoot);
1399
+ this.cache.apiRoutes = apiRoutes;
1083
1400
  return apiRoutes;
1084
1401
  }
1085
1402
  loadWssRoutes() {
1403
+ if (this.cache.wssRoutes) {
1404
+ return this.cache.wssRoutes;
1405
+ }
1086
1406
  const { wssRoutes } = loadRoutesFromManifest(this.projectRoot);
1407
+ this.cache.wssRoutes = wssRoutes;
1087
1408
  return wssRoutes;
1088
1409
  }
1089
1410
  loadNotFoundRoute() {
1090
- return loadNotFoundFromManifest(this.projectRoot);
1411
+ if (this.cache.notFoundRoute !== void 0) {
1412
+ return this.cache.notFoundRoute;
1413
+ }
1414
+ const route = loadNotFoundFromManifest(this.projectRoot);
1415
+ this.cache.notFoundRoute = route;
1416
+ return route;
1091
1417
  }
1092
1418
  loadErrorRoute() {
1093
- return loadErrorFromManifest(this.projectRoot);
1419
+ if (this.cache.errorRoute !== void 0) {
1420
+ return this.cache.errorRoute;
1421
+ }
1422
+ const route = loadErrorFromManifest(this.projectRoot);
1423
+ this.cache.errorRoute = route;
1424
+ return route;
1094
1425
  }
1095
1426
  loadRouteChunks() {
1096
- return loadChunksFromManifest(this.projectRoot);
1427
+ if (this.cache.routeChunks) {
1428
+ return this.cache.routeChunks;
1429
+ }
1430
+ const chunks = loadChunksFromManifest(this.projectRoot);
1431
+ this.cache.routeChunks = chunks;
1432
+ return chunks;
1097
1433
  }
1098
1434
  };
1099
1435
  function loadNotFoundRouteFromFilesystem(appDir) {
@@ -1128,7 +1464,12 @@ function loadNotFoundRouteFromFilesystem(appDir) {
1128
1464
  notFoundDir,
1129
1465
  appDir
1130
1466
  );
1131
- const { middlewares, loader, dynamic, generateStaticParams } = loadLoaderForDir(notFoundDir);
1467
+ const layoutServerHooks = [];
1468
+ for (const layoutFile of layoutFiles) {
1469
+ const layoutServerHook = loadLayoutServerHook(layoutFile);
1470
+ layoutServerHooks.push(layoutServerHook);
1471
+ }
1472
+ const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(notFoundDir);
1132
1473
  return {
1133
1474
  pattern: NOT_FOUND_PATTERN,
1134
1475
  regex: new RegExp(`^${NOT_FOUND_PATTERN}/?$`),
@@ -1138,7 +1479,10 @@ function loadNotFoundRouteFromFilesystem(appDir) {
1138
1479
  pageFile: notFoundFile,
1139
1480
  layoutFiles,
1140
1481
  middlewares,
1141
- loader,
1482
+ loader: serverHook,
1483
+ // Keep 'loader' field name for backward compatibility
1484
+ layoutServerHooks,
1485
+ // Server hooks for each layout (same order as layouts)
1142
1486
  dynamic,
1143
1487
  generateStaticParams
1144
1488
  };
@@ -1169,7 +1513,12 @@ function loadErrorRouteFromFilesystem(appDir) {
1169
1513
  appDir,
1170
1514
  appDir
1171
1515
  );
1172
- const { middlewares, loader, dynamic, generateStaticParams } = loadLoaderForDir(appDir);
1516
+ const layoutServerHooks = [];
1517
+ for (const layoutFile of layoutFiles) {
1518
+ const layoutServerHook = loadLayoutServerHook(layoutFile);
1519
+ layoutServerHooks.push(layoutServerHook);
1520
+ }
1521
+ const { middlewares, serverHook, dynamic, generateStaticParams } = loadServerHookForDir(appDir);
1173
1522
  return {
1174
1523
  pattern: ERROR_PATTERN,
1175
1524
  regex: new RegExp(`^${ERROR_PATTERN}/?$`),
@@ -1179,7 +1528,10 @@ function loadErrorRouteFromFilesystem(appDir) {
1179
1528
  pageFile: errorFile,
1180
1529
  layoutFiles,
1181
1530
  middlewares,
1182
- loader,
1531
+ loader: serverHook,
1532
+ // Keep 'loader' field name for backward compatibility
1533
+ layoutServerHooks,
1534
+ // Server hooks for each layout (same order as layouts)
1183
1535
  dynamic,
1184
1536
  generateStaticParams
1185
1537
  };
@@ -1210,7 +1562,8 @@ function loadAliasesFromTsconfig(projectRoot) {
1210
1562
  try {
1211
1563
  tsconfig = JSON.parse(import_fs11.default.readFileSync(tsconfigPath, "utf-8"));
1212
1564
  } catch (err) {
1213
- console.warn("[framework] Could not read tsconfig.json:", err);
1565
+ console.warn("\u26A0\uFE0F [framework] Could not read tsconfig.json:", err instanceof Error ? err.message : String(err));
1566
+ console.warn("\u{1F4A1} Using default path aliases. For custom aliases, ensure tsconfig.json is valid.");
1214
1567
  aliases["@app"] = import_path13.default.resolve(projectRoot, "app");
1215
1568
  return aliases;
1216
1569
  }
@@ -1264,7 +1617,7 @@ function copyStaticAssets(projectRoot, outDir) {
1264
1617
  }
1265
1618
  }
1266
1619
  }
1267
- function generateAssetManifest(outDir) {
1620
+ function generateAssetManifest(outDir, stats) {
1268
1621
  const manifest = {
1269
1622
  client: {
1270
1623
  js: "client.js",
@@ -1276,27 +1629,128 @@ function generateAssetManifest(outDir) {
1276
1629
  return manifest;
1277
1630
  }
1278
1631
  const files = import_fs11.default.readdirSync(outDir);
1279
- const clientJsMatch = files.find((f) => /^client\.[\w-]+\.js$/.test(f) || f === "client.js");
1280
- if (clientJsMatch) {
1281
- manifest.client.js = clientJsMatch;
1632
+ if (stats) {
1633
+ try {
1634
+ const statsJson = stats.toJson({
1635
+ all: false,
1636
+ entrypoints: true,
1637
+ assets: true,
1638
+ chunks: true,
1639
+ chunkRelations: true
1640
+ // Include chunk dependencies
1641
+ });
1642
+ const clientEntrypoint = statsJson.entrypoints?.client;
1643
+ if (clientEntrypoint?.assets) {
1644
+ const clientJsFiles = clientEntrypoint.assets.map((asset) => typeof asset === "string" ? asset : asset.name).filter((name) => name.endsWith(".js"));
1645
+ if (statsJson.chunks && clientEntrypoint.chunks) {
1646
+ const entrypointChunkIds = new Set(
1647
+ Array.isArray(clientEntrypoint.chunks) ? clientEntrypoint.chunks : [clientEntrypoint.chunks]
1648
+ );
1649
+ const dependencyChunks = [];
1650
+ for (const chunk of statsJson.chunks) {
1651
+ if (chunk.id && entrypointChunkIds.has(chunk.id)) {
1652
+ if (chunk.files) {
1653
+ const jsFiles = chunk.files.filter((f) => f.endsWith(".js")).filter((f) => !clientJsFiles.includes(f));
1654
+ dependencyChunks.push(...jsFiles);
1655
+ }
1656
+ }
1657
+ }
1658
+ const visitedChunkIds = new Set(entrypointChunkIds);
1659
+ const chunksToCheck = Array.from(entrypointChunkIds);
1660
+ while (chunksToCheck.length > 0) {
1661
+ const chunkId = chunksToCheck.shift();
1662
+ if (!chunkId) continue;
1663
+ const chunk = statsJson.chunks?.find((c) => c.id === chunkId);
1664
+ if (chunk?.children) {
1665
+ const children = Array.isArray(chunk.children) ? chunk.children : [chunk.children];
1666
+ for (const childId of children) {
1667
+ if (!visitedChunkIds.has(childId)) {
1668
+ visitedChunkIds.add(childId);
1669
+ chunksToCheck.push(childId);
1670
+ const childChunk = statsJson.chunks?.find((c) => c.id === childId);
1671
+ if (childChunk?.files) {
1672
+ const jsFiles = childChunk.files.filter((f) => f.endsWith(".js")).filter((f) => !clientJsFiles.includes(f) && !dependencyChunks.includes(f));
1673
+ dependencyChunks.push(...jsFiles);
1674
+ }
1675
+ }
1676
+ }
1677
+ }
1678
+ }
1679
+ if (dependencyChunks.length > 0) {
1680
+ clientJsFiles.splice(-1, 0, ...dependencyChunks);
1681
+ }
1682
+ }
1683
+ if (clientJsFiles.length > 0) {
1684
+ manifest.entrypoints = {
1685
+ client: clientJsFiles
1686
+ };
1687
+ manifest.client.js = clientJsFiles[clientJsFiles.length - 1];
1688
+ const clientCssFiles = clientEntrypoint.assets.map((asset) => typeof asset === "string" ? asset : asset.name).filter((name) => name.endsWith(".css"));
1689
+ if (clientCssFiles.length > 0) {
1690
+ manifest.client.css = clientCssFiles[0];
1691
+ }
1692
+ }
1693
+ }
1694
+ } catch (err) {
1695
+ console.warn("[framework] Failed to extract entrypoints from stats, falling back to file scanning:", err);
1696
+ }
1697
+ }
1698
+ if (!manifest.client.js) {
1699
+ const clientJsMatch = files.find((f) => /^client\.[\w-]+\.js$/.test(f) || f === "client.js");
1700
+ if (clientJsMatch) {
1701
+ manifest.client.js = clientJsMatch;
1702
+ }
1282
1703
  }
1283
- const clientCssMatch = files.find((f) => /^client\.[\w-]+\.css$/.test(f) || f === "client.css");
1284
- if (clientCssMatch) {
1285
- manifest.client.css = clientCssMatch;
1704
+ if (!manifest.client.css) {
1705
+ const clientCssMatch = files.find((f) => /^client\.[\w-]+\.css$/.test(f) || f === "client.css");
1706
+ if (clientCssMatch) {
1707
+ manifest.client.css = clientCssMatch;
1708
+ }
1286
1709
  }
1710
+ const sharedChunksToAdd = [];
1287
1711
  for (const file of files) {
1288
1712
  if (!file.endsWith(".js")) continue;
1289
1713
  if (file === manifest.client.js) continue;
1714
+ if (manifest.entrypoints?.client?.includes(file)) continue;
1290
1715
  const routeMatch = file.match(/^(route-[^.]+)(\.[\w-]+)?\.js$/);
1291
1716
  if (routeMatch) {
1292
1717
  const chunkName = routeMatch[1];
1293
1718
  manifest.chunks[chunkName] = file;
1294
1719
  continue;
1295
1720
  }
1721
+ const vendorMatch = file.match(/^(vendor)(\.[\w-]+)?\.js$/);
1722
+ if (vendorMatch) {
1723
+ const chunkName = vendorMatch[1];
1724
+ manifest.chunks[chunkName] = file;
1725
+ sharedChunksToAdd.push(file);
1726
+ continue;
1727
+ }
1728
+ const vendorCommonsMatch = file.match(/^(vendor-commons)(\.[\w-]+)?\.js$/);
1729
+ if (vendorCommonsMatch) {
1730
+ const chunkName = vendorCommonsMatch[1];
1731
+ manifest.chunks[chunkName] = file;
1732
+ sharedChunksToAdd.push(file);
1733
+ continue;
1734
+ }
1296
1735
  const numericMatch = file.match(/^(\d+)(\.[\w-]+)?\.js$/);
1297
1736
  if (numericMatch) {
1298
1737
  const chunkName = numericMatch[1];
1299
1738
  manifest.chunks[chunkName] = file;
1739
+ sharedChunksToAdd.push(file);
1740
+ continue;
1741
+ }
1742
+ }
1743
+ if (sharedChunksToAdd.length > 0 && manifest.entrypoints?.client) {
1744
+ const entrypoints = manifest.entrypoints.client;
1745
+ const mainEntry = entrypoints[entrypoints.length - 1];
1746
+ const uniqueShared = sharedChunksToAdd.filter((f) => !entrypoints.includes(f));
1747
+ entrypoints.splice(-1, 0, ...uniqueShared);
1748
+ if (entrypoints[entrypoints.length - 1] !== mainEntry) {
1749
+ const mainIndex = entrypoints.indexOf(mainEntry);
1750
+ if (mainIndex >= 0) {
1751
+ entrypoints.splice(mainIndex, 1);
1752
+ }
1753
+ entrypoints.push(mainEntry);
1300
1754
  }
1301
1755
  }
1302
1756
  return manifest;
@@ -1336,9 +1790,7 @@ function createClientConfig(projectRoot, mode) {
1336
1790
  const outDir = import_path14.default.join(buildDir, "client");
1337
1791
  const envPath2 = import_path14.default.join(projectRoot, ".env");
1338
1792
  if (import_fs12.default.existsSync(envPath2)) {
1339
- import_dotenv.default.config({
1340
- path: envPath2
1341
- });
1793
+ import_dotenv.default.config({ path: envPath2 });
1342
1794
  }
1343
1795
  const publicEnv = {};
1344
1796
  for (const [key, value] of Object.entries(process.env)) {
@@ -1397,14 +1849,57 @@ function createClientConfig(projectRoot, mode) {
1397
1849
  },
1398
1850
  plugins: [
1399
1851
  new import_core.rspack.DefinePlugin({
1400
- // Always define NODE_ENV, using mode as fallback if not set
1401
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || mode),
1852
+ // Use mode directly to ensure development mode is correctly set
1853
+ // This replaces process.env.NODE_ENV in the client bundle with the literal string value
1854
+ "process.env.NODE_ENV": JSON.stringify(mode),
1402
1855
  ...publicEnv
1403
1856
  }),
1404
1857
  new import_core.rspack.CssExtractRspackPlugin({
1405
1858
  filename: mode === "production" ? "client.[contenthash].css" : "client.css"
1406
1859
  })
1407
1860
  ],
1861
+ optimization: mode === "production" ? {
1862
+ usedExports: true,
1863
+ sideEffects: false,
1864
+ // More aggressive tree shaking - assume no side effects
1865
+ providedExports: true,
1866
+ concatenateModules: true,
1867
+ // Better for tree shaking
1868
+ minimize: true,
1869
+ removeEmptyChunks: true,
1870
+ mergeDuplicateChunks: true,
1871
+ // Improved code splitting: separate vendor chunks for better caching
1872
+ splitChunks: {
1873
+ chunks: "all",
1874
+ cacheGroups: {
1875
+ // Separate React/React-DOM into dedicated vendor chunk
1876
+ // This improves caching: React rarely changes, so users don't need to re-download it
1877
+ vendor: {
1878
+ test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
1879
+ name: "vendor",
1880
+ priority: 30,
1881
+ enforce: true,
1882
+ // Force separation even if only used once
1883
+ reuseExistingChunk: true
1884
+ },
1885
+ // Other node_modules dependencies in a separate chunk
1886
+ vendorCommons: {
1887
+ test: /[\\/]node_modules[\\/](?!(react|react-dom|scheduler)[\\/])/,
1888
+ name: "vendor-commons",
1889
+ priority: 10,
1890
+ minChunks: 2,
1891
+ // Only create if used in 2+ chunks
1892
+ reuseExistingChunk: true
1893
+ },
1894
+ // Default: shared application code (not in node_modules)
1895
+ default: {
1896
+ minChunks: 2,
1897
+ priority: 5,
1898
+ reuseExistingChunk: true
1899
+ }
1900
+ }
1901
+ }
1902
+ } : void 0,
1408
1903
  infrastructureLogging: {
1409
1904
  level: "error"
1410
1905
  },
@@ -1433,7 +1928,13 @@ function startClientBundler(projectRoot, mode = "development") {
1433
1928
  });
1434
1929
  compiler.watch({}, (err, stats) => {
1435
1930
  if (err) {
1436
- console.error("[framework][client] Rspack error:", err);
1931
+ console.error("\n\u274C [framework][client] Rspack compilation error:");
1932
+ console.error(err);
1933
+ console.error("\n\u{1F4A1} Suggestions:");
1934
+ console.error(" \u2022 Check for syntax errors in your code");
1935
+ console.error(" \u2022 Verify all imports are correct");
1936
+ console.error(" \u2022 Ensure all dependencies are installed");
1937
+ console.error(" \u2022 Try deleting .loly folder and rebuilding\n");
1437
1938
  isBuilding = false;
1438
1939
  lastBuildTime = Date.now();
1439
1940
  if (buildResolve) {
@@ -1444,17 +1945,20 @@ function startClientBundler(projectRoot, mode = "development") {
1444
1945
  return;
1445
1946
  }
1446
1947
  if (!stats) {
1948
+ console.warn("\u26A0\uFE0F [framework][client] Build completed but no stats available");
1447
1949
  isBuilding = false;
1448
1950
  lastBuildTime = Date.now();
1449
1951
  return;
1450
1952
  }
1451
1953
  if (stats.hasErrors()) {
1452
- console.error(
1453
- "[framework][client] Build with errors:\n",
1454
- stats.toString("errors-only")
1455
- );
1954
+ console.error("\n\u274C [framework][client] Build failed with errors:\n");
1955
+ console.error(stats.toString("errors-only"));
1956
+ console.error("\n\u{1F4A1} Common fixes:");
1957
+ console.error(" \u2022 Fix syntax errors shown above");
1958
+ console.error(" \u2022 Check for missing imports or dependencies");
1959
+ console.error(" \u2022 Verify TypeScript types are correct\n");
1456
1960
  } else {
1457
- console.log("[framework][client] \u2713 Client bundle rebuilt successfully");
1961
+ console.log("\u2705 [framework][client] Client bundle rebuilt successfully");
1458
1962
  }
1459
1963
  isBuilding = false;
1460
1964
  lastBuildTime = Date.now();
@@ -1489,23 +1993,33 @@ function buildClientBundle(projectRoot) {
1489
1993
  compiler.close(() => {
1490
1994
  });
1491
1995
  if (err) {
1492
- console.error("[framework][client] Build error:", err);
1996
+ console.error("\n\u274C [framework][client] Production build error:");
1997
+ console.error(err);
1998
+ console.error("\n\u{1F4A1} Suggestions:");
1999
+ console.error(" \u2022 Check for syntax errors in your code");
2000
+ console.error(" \u2022 Verify all imports are correct");
2001
+ console.error(" \u2022 Ensure all dependencies are installed");
2002
+ console.error(" \u2022 Review the error details above\n");
1493
2003
  return reject(err);
1494
2004
  }
1495
2005
  if (!stats) {
1496
- const error = new Error("No stats from Rspack");
1497
- console.error("[framework][client] Build error:", error);
2006
+ const error = new Error("No stats from Rspack - build may have failed silently");
2007
+ console.error("\n\u274C [framework][client] Build error:", error.message);
2008
+ console.error("\u{1F4A1} Try rebuilding or check Rspack configuration\n");
1498
2009
  return reject(error);
1499
2010
  }
1500
2011
  if (stats.hasErrors()) {
1501
- console.error(
1502
- "[framework][client] Build with errors:\n",
1503
- stats.toString("errors-only")
1504
- );
1505
- return reject(new Error("Client build failed"));
2012
+ console.error("\n\u274C [framework][client] Production build failed:\n");
2013
+ console.error(stats.toString("errors-only"));
2014
+ console.error("\n\u{1F4A1} Common fixes:");
2015
+ console.error(" \u2022 Fix syntax errors shown above");
2016
+ console.error(" \u2022 Check for missing imports or dependencies");
2017
+ console.error(" \u2022 Verify TypeScript types are correct");
2018
+ console.error(" \u2022 Review build configuration\n");
2019
+ return reject(new Error("Client build failed - see errors above"));
1506
2020
  }
1507
2021
  copyStaticAssets(projectRoot, outDir);
1508
- const assetManifest = generateAssetManifest(outDir);
2022
+ const assetManifest = generateAssetManifest(outDir, stats);
1509
2023
  const manifestPath = import_path15.default.join(projectRoot, BUILD_FOLDER_NAME, "asset-manifest.json");
1510
2024
  import_fs13.default.writeFileSync(manifestPath, JSON.stringify(assetManifest, null, 2), "utf-8");
1511
2025
  resolve3({ outDir });
@@ -1589,7 +2103,7 @@ var ReaddirpStream = class extends import_node_stream.Readable {
1589
2103
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
1590
2104
  const statMethod = opts.lstat ? import_promises.lstat : import_promises.stat;
1591
2105
  if (wantBigintFsStats) {
1592
- this._stat = (path25) => statMethod(path25, { bigint: true });
2106
+ this._stat = (path28) => statMethod(path28, { bigint: true });
1593
2107
  } else {
1594
2108
  this._stat = statMethod;
1595
2109
  }
@@ -1614,8 +2128,8 @@ var ReaddirpStream = class extends import_node_stream.Readable {
1614
2128
  const par = this.parent;
1615
2129
  const fil = par && par.files;
1616
2130
  if (fil && fil.length > 0) {
1617
- const { path: path25, depth } = par;
1618
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path25));
2131
+ const { path: path28, depth } = par;
2132
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path28));
1619
2133
  const awaited = await Promise.all(slice);
1620
2134
  for (const entry of awaited) {
1621
2135
  if (!entry)
@@ -1655,20 +2169,20 @@ var ReaddirpStream = class extends import_node_stream.Readable {
1655
2169
  this.reading = false;
1656
2170
  }
1657
2171
  }
1658
- async _exploreDir(path25, depth) {
2172
+ async _exploreDir(path28, depth) {
1659
2173
  let files;
1660
2174
  try {
1661
- files = await (0, import_promises.readdir)(path25, this._rdOptions);
2175
+ files = await (0, import_promises.readdir)(path28, this._rdOptions);
1662
2176
  } catch (error) {
1663
2177
  this._onError(error);
1664
2178
  }
1665
- return { files, depth, path: path25 };
2179
+ return { files, depth, path: path28 };
1666
2180
  }
1667
- async _formatEntry(dirent, path25) {
2181
+ async _formatEntry(dirent, path28) {
1668
2182
  let entry;
1669
2183
  const basename3 = this._isDirent ? dirent.name : dirent;
1670
2184
  try {
1671
- const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path25, basename3));
2185
+ const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path28, basename3));
1672
2186
  entry = { path: (0, import_node_path.relative)(this._root, fullPath), fullPath, basename: basename3 };
1673
2187
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
1674
2188
  } catch (err) {
@@ -2068,16 +2582,16 @@ var delFromSet = (main, prop, item) => {
2068
2582
  };
2069
2583
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
2070
2584
  var FsWatchInstances = /* @__PURE__ */ new Map();
2071
- function createFsWatchInstance(path25, options, listener, errHandler, emitRaw) {
2585
+ function createFsWatchInstance(path28, options, listener, errHandler, emitRaw) {
2072
2586
  const handleEvent = (rawEvent, evPath) => {
2073
- listener(path25);
2074
- emitRaw(rawEvent, evPath, { watchedPath: path25 });
2075
- if (evPath && path25 !== evPath) {
2076
- fsWatchBroadcast(sysPath.resolve(path25, evPath), KEY_LISTENERS, sysPath.join(path25, evPath));
2587
+ listener(path28);
2588
+ emitRaw(rawEvent, evPath, { watchedPath: path28 });
2589
+ if (evPath && path28 !== evPath) {
2590
+ fsWatchBroadcast(sysPath.resolve(path28, evPath), KEY_LISTENERS, sysPath.join(path28, evPath));
2077
2591
  }
2078
2592
  };
2079
2593
  try {
2080
- return (0, import_fs14.watch)(path25, {
2594
+ return (0, import_fs14.watch)(path28, {
2081
2595
  persistent: options.persistent
2082
2596
  }, handleEvent);
2083
2597
  } catch (error) {
@@ -2093,12 +2607,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
2093
2607
  listener(val1, val2, val3);
2094
2608
  });
2095
2609
  };
2096
- var setFsWatchListener = (path25, fullPath, options, handlers) => {
2610
+ var setFsWatchListener = (path28, fullPath, options, handlers) => {
2097
2611
  const { listener, errHandler, rawEmitter } = handlers;
2098
2612
  let cont = FsWatchInstances.get(fullPath);
2099
2613
  let watcher;
2100
2614
  if (!options.persistent) {
2101
- watcher = createFsWatchInstance(path25, options, listener, errHandler, rawEmitter);
2615
+ watcher = createFsWatchInstance(path28, options, listener, errHandler, rawEmitter);
2102
2616
  if (!watcher)
2103
2617
  return;
2104
2618
  return watcher.close.bind(watcher);
@@ -2109,7 +2623,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2109
2623
  addAndConvert(cont, KEY_RAW, rawEmitter);
2110
2624
  } else {
2111
2625
  watcher = createFsWatchInstance(
2112
- path25,
2626
+ path28,
2113
2627
  options,
2114
2628
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
2115
2629
  errHandler,
@@ -2124,7 +2638,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2124
2638
  cont.watcherUnusable = true;
2125
2639
  if (isWindows && error.code === "EPERM") {
2126
2640
  try {
2127
- const fd = await (0, import_promises2.open)(path25, "r");
2641
+ const fd = await (0, import_promises2.open)(path28, "r");
2128
2642
  await fd.close();
2129
2643
  broadcastErr(error);
2130
2644
  } catch (err) {
@@ -2155,7 +2669,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2155
2669
  };
2156
2670
  };
2157
2671
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
2158
- var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
2672
+ var setFsWatchFileListener = (path28, fullPath, options, handlers) => {
2159
2673
  const { listener, rawEmitter } = handlers;
2160
2674
  let cont = FsWatchFileInstances.get(fullPath);
2161
2675
  const copts = cont && cont.options;
@@ -2177,7 +2691,7 @@ var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
2177
2691
  });
2178
2692
  const currmtime = curr.mtimeMs;
2179
2693
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
2180
- foreach(cont.listeners, (listener2) => listener2(path25, curr));
2694
+ foreach(cont.listeners, (listener2) => listener2(path28, curr));
2181
2695
  }
2182
2696
  })
2183
2697
  };
@@ -2205,13 +2719,13 @@ var NodeFsHandler = class {
2205
2719
  * @param listener on fs change
2206
2720
  * @returns closer for the watcher instance
2207
2721
  */
2208
- _watchWithNodeFs(path25, listener) {
2722
+ _watchWithNodeFs(path28, listener) {
2209
2723
  const opts = this.fsw.options;
2210
- const directory = sysPath.dirname(path25);
2211
- const basename3 = sysPath.basename(path25);
2724
+ const directory = sysPath.dirname(path28);
2725
+ const basename3 = sysPath.basename(path28);
2212
2726
  const parent = this.fsw._getWatchedDir(directory);
2213
2727
  parent.add(basename3);
2214
- const absolutePath = sysPath.resolve(path25);
2728
+ const absolutePath = sysPath.resolve(path28);
2215
2729
  const options = {
2216
2730
  persistent: opts.persistent
2217
2731
  };
@@ -2221,12 +2735,12 @@ var NodeFsHandler = class {
2221
2735
  if (opts.usePolling) {
2222
2736
  const enableBin = opts.interval !== opts.binaryInterval;
2223
2737
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
2224
- closer = setFsWatchFileListener(path25, absolutePath, options, {
2738
+ closer = setFsWatchFileListener(path28, absolutePath, options, {
2225
2739
  listener,
2226
2740
  rawEmitter: this.fsw._emitRaw
2227
2741
  });
2228
2742
  } else {
2229
- closer = setFsWatchListener(path25, absolutePath, options, {
2743
+ closer = setFsWatchListener(path28, absolutePath, options, {
2230
2744
  listener,
2231
2745
  errHandler: this._boundHandleError,
2232
2746
  rawEmitter: this.fsw._emitRaw
@@ -2248,7 +2762,7 @@ var NodeFsHandler = class {
2248
2762
  let prevStats = stats;
2249
2763
  if (parent.has(basename3))
2250
2764
  return;
2251
- const listener = async (path25, newStats) => {
2765
+ const listener = async (path28, newStats) => {
2252
2766
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
2253
2767
  return;
2254
2768
  if (!newStats || newStats.mtimeMs === 0) {
@@ -2262,11 +2776,11 @@ var NodeFsHandler = class {
2262
2776
  this.fsw._emit(EV.CHANGE, file, newStats2);
2263
2777
  }
2264
2778
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
2265
- this.fsw._closeFile(path25);
2779
+ this.fsw._closeFile(path28);
2266
2780
  prevStats = newStats2;
2267
2781
  const closer2 = this._watchWithNodeFs(file, listener);
2268
2782
  if (closer2)
2269
- this.fsw._addPathCloser(path25, closer2);
2783
+ this.fsw._addPathCloser(path28, closer2);
2270
2784
  } else {
2271
2785
  prevStats = newStats2;
2272
2786
  }
@@ -2298,7 +2812,7 @@ var NodeFsHandler = class {
2298
2812
  * @param item basename of this item
2299
2813
  * @returns true if no more processing is needed for this entry.
2300
2814
  */
2301
- async _handleSymlink(entry, directory, path25, item) {
2815
+ async _handleSymlink(entry, directory, path28, item) {
2302
2816
  if (this.fsw.closed) {
2303
2817
  return;
2304
2818
  }
@@ -2308,7 +2822,7 @@ var NodeFsHandler = class {
2308
2822
  this.fsw._incrReadyCount();
2309
2823
  let linkPath;
2310
2824
  try {
2311
- linkPath = await (0, import_promises2.realpath)(path25);
2825
+ linkPath = await (0, import_promises2.realpath)(path28);
2312
2826
  } catch (e) {
2313
2827
  this.fsw._emitReady();
2314
2828
  return true;
@@ -2318,12 +2832,12 @@ var NodeFsHandler = class {
2318
2832
  if (dir.has(item)) {
2319
2833
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
2320
2834
  this.fsw._symlinkPaths.set(full, linkPath);
2321
- this.fsw._emit(EV.CHANGE, path25, entry.stats);
2835
+ this.fsw._emit(EV.CHANGE, path28, entry.stats);
2322
2836
  }
2323
2837
  } else {
2324
2838
  dir.add(item);
2325
2839
  this.fsw._symlinkPaths.set(full, linkPath);
2326
- this.fsw._emit(EV.ADD, path25, entry.stats);
2840
+ this.fsw._emit(EV.ADD, path28, entry.stats);
2327
2841
  }
2328
2842
  this.fsw._emitReady();
2329
2843
  return true;
@@ -2352,9 +2866,9 @@ var NodeFsHandler = class {
2352
2866
  return;
2353
2867
  }
2354
2868
  const item = entry.path;
2355
- let path25 = sysPath.join(directory, item);
2869
+ let path28 = sysPath.join(directory, item);
2356
2870
  current.add(item);
2357
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path25, item)) {
2871
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path28, item)) {
2358
2872
  return;
2359
2873
  }
2360
2874
  if (this.fsw.closed) {
@@ -2363,8 +2877,8 @@ var NodeFsHandler = class {
2363
2877
  }
2364
2878
  if (item === target || !target && !previous.has(item)) {
2365
2879
  this.fsw._incrReadyCount();
2366
- path25 = sysPath.join(dir, sysPath.relative(dir, path25));
2367
- this._addToNodeFs(path25, initialAdd, wh, depth + 1);
2880
+ path28 = sysPath.join(dir, sysPath.relative(dir, path28));
2881
+ this._addToNodeFs(path28, initialAdd, wh, depth + 1);
2368
2882
  }
2369
2883
  }).on(EV.ERROR, this._boundHandleError);
2370
2884
  return new Promise((resolve3, reject) => {
@@ -2433,13 +2947,13 @@ var NodeFsHandler = class {
2433
2947
  * @param depth Child path actually targeted for watch
2434
2948
  * @param target Child path actually targeted for watch
2435
2949
  */
2436
- async _addToNodeFs(path25, initialAdd, priorWh, depth, target) {
2950
+ async _addToNodeFs(path28, initialAdd, priorWh, depth, target) {
2437
2951
  const ready = this.fsw._emitReady;
2438
- if (this.fsw._isIgnored(path25) || this.fsw.closed) {
2952
+ if (this.fsw._isIgnored(path28) || this.fsw.closed) {
2439
2953
  ready();
2440
2954
  return false;
2441
2955
  }
2442
- const wh = this.fsw._getWatchHelpers(path25);
2956
+ const wh = this.fsw._getWatchHelpers(path28);
2443
2957
  if (priorWh) {
2444
2958
  wh.filterPath = (entry) => priorWh.filterPath(entry);
2445
2959
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -2455,8 +2969,8 @@ var NodeFsHandler = class {
2455
2969
  const follow = this.fsw.options.followSymlinks;
2456
2970
  let closer;
2457
2971
  if (stats.isDirectory()) {
2458
- const absPath = sysPath.resolve(path25);
2459
- const targetPath = follow ? await (0, import_promises2.realpath)(path25) : path25;
2972
+ const absPath = sysPath.resolve(path28);
2973
+ const targetPath = follow ? await (0, import_promises2.realpath)(path28) : path28;
2460
2974
  if (this.fsw.closed)
2461
2975
  return;
2462
2976
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -2466,29 +2980,29 @@ var NodeFsHandler = class {
2466
2980
  this.fsw._symlinkPaths.set(absPath, targetPath);
2467
2981
  }
2468
2982
  } else if (stats.isSymbolicLink()) {
2469
- const targetPath = follow ? await (0, import_promises2.realpath)(path25) : path25;
2983
+ const targetPath = follow ? await (0, import_promises2.realpath)(path28) : path28;
2470
2984
  if (this.fsw.closed)
2471
2985
  return;
2472
2986
  const parent = sysPath.dirname(wh.watchPath);
2473
2987
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
2474
2988
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
2475
- closer = await this._handleDir(parent, stats, initialAdd, depth, path25, wh, targetPath);
2989
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path28, wh, targetPath);
2476
2990
  if (this.fsw.closed)
2477
2991
  return;
2478
2992
  if (targetPath !== void 0) {
2479
- this.fsw._symlinkPaths.set(sysPath.resolve(path25), targetPath);
2993
+ this.fsw._symlinkPaths.set(sysPath.resolve(path28), targetPath);
2480
2994
  }
2481
2995
  } else {
2482
2996
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
2483
2997
  }
2484
2998
  ready();
2485
2999
  if (closer)
2486
- this.fsw._addPathCloser(path25, closer);
3000
+ this.fsw._addPathCloser(path28, closer);
2487
3001
  return false;
2488
3002
  } catch (error) {
2489
3003
  if (this.fsw._handleError(error)) {
2490
3004
  ready();
2491
- return path25;
3005
+ return path28;
2492
3006
  }
2493
3007
  }
2494
3008
  }
@@ -2531,26 +3045,26 @@ function createPattern(matcher) {
2531
3045
  }
2532
3046
  return () => false;
2533
3047
  }
2534
- function normalizePath(path25) {
2535
- if (typeof path25 !== "string")
3048
+ function normalizePath(path28) {
3049
+ if (typeof path28 !== "string")
2536
3050
  throw new Error("string expected");
2537
- path25 = sysPath2.normalize(path25);
2538
- path25 = path25.replace(/\\/g, "/");
3051
+ path28 = sysPath2.normalize(path28);
3052
+ path28 = path28.replace(/\\/g, "/");
2539
3053
  let prepend = false;
2540
- if (path25.startsWith("//"))
3054
+ if (path28.startsWith("//"))
2541
3055
  prepend = true;
2542
3056
  const DOUBLE_SLASH_RE2 = /\/\//;
2543
- while (path25.match(DOUBLE_SLASH_RE2))
2544
- path25 = path25.replace(DOUBLE_SLASH_RE2, "/");
3057
+ while (path28.match(DOUBLE_SLASH_RE2))
3058
+ path28 = path28.replace(DOUBLE_SLASH_RE2, "/");
2545
3059
  if (prepend)
2546
- path25 = "/" + path25;
2547
- return path25;
3060
+ path28 = "/" + path28;
3061
+ return path28;
2548
3062
  }
2549
3063
  function matchPatterns(patterns, testString, stats) {
2550
- const path25 = normalizePath(testString);
3064
+ const path28 = normalizePath(testString);
2551
3065
  for (let index = 0; index < patterns.length; index++) {
2552
3066
  const pattern = patterns[index];
2553
- if (pattern(path25, stats)) {
3067
+ if (pattern(path28, stats)) {
2554
3068
  return true;
2555
3069
  }
2556
3070
  }
@@ -2590,19 +3104,19 @@ var toUnix = (string) => {
2590
3104
  }
2591
3105
  return str;
2592
3106
  };
2593
- var normalizePathToUnix = (path25) => toUnix(sysPath2.normalize(toUnix(path25)));
2594
- var normalizeIgnored = (cwd = "") => (path25) => {
2595
- if (typeof path25 === "string") {
2596
- return normalizePathToUnix(sysPath2.isAbsolute(path25) ? path25 : sysPath2.join(cwd, path25));
3107
+ var normalizePathToUnix = (path28) => toUnix(sysPath2.normalize(toUnix(path28)));
3108
+ var normalizeIgnored = (cwd = "") => (path28) => {
3109
+ if (typeof path28 === "string") {
3110
+ return normalizePathToUnix(sysPath2.isAbsolute(path28) ? path28 : sysPath2.join(cwd, path28));
2597
3111
  } else {
2598
- return path25;
3112
+ return path28;
2599
3113
  }
2600
3114
  };
2601
- var getAbsolutePath = (path25, cwd) => {
2602
- if (sysPath2.isAbsolute(path25)) {
2603
- return path25;
3115
+ var getAbsolutePath = (path28, cwd) => {
3116
+ if (sysPath2.isAbsolute(path28)) {
3117
+ return path28;
2604
3118
  }
2605
- return sysPath2.join(cwd, path25);
3119
+ return sysPath2.join(cwd, path28);
2606
3120
  };
2607
3121
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
2608
3122
  var DirEntry = class {
@@ -2657,10 +3171,10 @@ var DirEntry = class {
2657
3171
  var STAT_METHOD_F = "stat";
2658
3172
  var STAT_METHOD_L = "lstat";
2659
3173
  var WatchHelper = class {
2660
- constructor(path25, follow, fsw) {
3174
+ constructor(path28, follow, fsw) {
2661
3175
  this.fsw = fsw;
2662
- const watchPath = path25;
2663
- this.path = path25 = path25.replace(REPLACER_RE, "");
3176
+ const watchPath = path28;
3177
+ this.path = path28 = path28.replace(REPLACER_RE, "");
2664
3178
  this.watchPath = watchPath;
2665
3179
  this.fullWatchPath = sysPath2.resolve(watchPath);
2666
3180
  this.dirParts = [];
@@ -2782,20 +3296,20 @@ var FSWatcher = class extends import_events.EventEmitter {
2782
3296
  this._closePromise = void 0;
2783
3297
  let paths = unifyPaths(paths_);
2784
3298
  if (cwd) {
2785
- paths = paths.map((path25) => {
2786
- const absPath = getAbsolutePath(path25, cwd);
3299
+ paths = paths.map((path28) => {
3300
+ const absPath = getAbsolutePath(path28, cwd);
2787
3301
  return absPath;
2788
3302
  });
2789
3303
  }
2790
- paths.forEach((path25) => {
2791
- this._removeIgnoredPath(path25);
3304
+ paths.forEach((path28) => {
3305
+ this._removeIgnoredPath(path28);
2792
3306
  });
2793
3307
  this._userIgnored = void 0;
2794
3308
  if (!this._readyCount)
2795
3309
  this._readyCount = 0;
2796
3310
  this._readyCount += paths.length;
2797
- Promise.all(paths.map(async (path25) => {
2798
- const res = await this._nodeFsHandler._addToNodeFs(path25, !_internal, void 0, 0, _origAdd);
3311
+ Promise.all(paths.map(async (path28) => {
3312
+ const res = await this._nodeFsHandler._addToNodeFs(path28, !_internal, void 0, 0, _origAdd);
2799
3313
  if (res)
2800
3314
  this._emitReady();
2801
3315
  return res;
@@ -2817,17 +3331,17 @@ var FSWatcher = class extends import_events.EventEmitter {
2817
3331
  return this;
2818
3332
  const paths = unifyPaths(paths_);
2819
3333
  const { cwd } = this.options;
2820
- paths.forEach((path25) => {
2821
- if (!sysPath2.isAbsolute(path25) && !this._closers.has(path25)) {
3334
+ paths.forEach((path28) => {
3335
+ if (!sysPath2.isAbsolute(path28) && !this._closers.has(path28)) {
2822
3336
  if (cwd)
2823
- path25 = sysPath2.join(cwd, path25);
2824
- path25 = sysPath2.resolve(path25);
3337
+ path28 = sysPath2.join(cwd, path28);
3338
+ path28 = sysPath2.resolve(path28);
2825
3339
  }
2826
- this._closePath(path25);
2827
- this._addIgnoredPath(path25);
2828
- if (this._watched.has(path25)) {
3340
+ this._closePath(path28);
3341
+ this._addIgnoredPath(path28);
3342
+ if (this._watched.has(path28)) {
2829
3343
  this._addIgnoredPath({
2830
- path: path25,
3344
+ path: path28,
2831
3345
  recursive: true
2832
3346
  });
2833
3347
  }
@@ -2891,38 +3405,38 @@ var FSWatcher = class extends import_events.EventEmitter {
2891
3405
  * @param stats arguments to be passed with event
2892
3406
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
2893
3407
  */
2894
- async _emit(event, path25, stats) {
3408
+ async _emit(event, path28, stats) {
2895
3409
  if (this.closed)
2896
3410
  return;
2897
3411
  const opts = this.options;
2898
3412
  if (isWindows)
2899
- path25 = sysPath2.normalize(path25);
3413
+ path28 = sysPath2.normalize(path28);
2900
3414
  if (opts.cwd)
2901
- path25 = sysPath2.relative(opts.cwd, path25);
2902
- const args = [path25];
3415
+ path28 = sysPath2.relative(opts.cwd, path28);
3416
+ const args = [path28];
2903
3417
  if (stats != null)
2904
3418
  args.push(stats);
2905
3419
  const awf = opts.awaitWriteFinish;
2906
3420
  let pw;
2907
- if (awf && (pw = this._pendingWrites.get(path25))) {
3421
+ if (awf && (pw = this._pendingWrites.get(path28))) {
2908
3422
  pw.lastChange = /* @__PURE__ */ new Date();
2909
3423
  return this;
2910
3424
  }
2911
3425
  if (opts.atomic) {
2912
3426
  if (event === EVENTS.UNLINK) {
2913
- this._pendingUnlinks.set(path25, [event, ...args]);
3427
+ this._pendingUnlinks.set(path28, [event, ...args]);
2914
3428
  setTimeout(() => {
2915
- this._pendingUnlinks.forEach((entry, path26) => {
3429
+ this._pendingUnlinks.forEach((entry, path29) => {
2916
3430
  this.emit(...entry);
2917
3431
  this.emit(EVENTS.ALL, ...entry);
2918
- this._pendingUnlinks.delete(path26);
3432
+ this._pendingUnlinks.delete(path29);
2919
3433
  });
2920
3434
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
2921
3435
  return this;
2922
3436
  }
2923
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path25)) {
3437
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path28)) {
2924
3438
  event = EVENTS.CHANGE;
2925
- this._pendingUnlinks.delete(path25);
3439
+ this._pendingUnlinks.delete(path28);
2926
3440
  }
2927
3441
  }
2928
3442
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -2940,16 +3454,16 @@ var FSWatcher = class extends import_events.EventEmitter {
2940
3454
  this.emitWithAll(event, args);
2941
3455
  }
2942
3456
  };
2943
- this._awaitWriteFinish(path25, awf.stabilityThreshold, event, awfEmit);
3457
+ this._awaitWriteFinish(path28, awf.stabilityThreshold, event, awfEmit);
2944
3458
  return this;
2945
3459
  }
2946
3460
  if (event === EVENTS.CHANGE) {
2947
- const isThrottled = !this._throttle(EVENTS.CHANGE, path25, 50);
3461
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path28, 50);
2948
3462
  if (isThrottled)
2949
3463
  return this;
2950
3464
  }
2951
3465
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
2952
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path25) : path25;
3466
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path28) : path28;
2953
3467
  let stats2;
2954
3468
  try {
2955
3469
  stats2 = await (0, import_promises3.stat)(fullPath);
@@ -2980,23 +3494,23 @@ var FSWatcher = class extends import_events.EventEmitter {
2980
3494
  * @param timeout duration of time to suppress duplicate actions
2981
3495
  * @returns tracking object or false if action should be suppressed
2982
3496
  */
2983
- _throttle(actionType, path25, timeout) {
3497
+ _throttle(actionType, path28, timeout) {
2984
3498
  if (!this._throttled.has(actionType)) {
2985
3499
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
2986
3500
  }
2987
3501
  const action = this._throttled.get(actionType);
2988
3502
  if (!action)
2989
3503
  throw new Error("invalid throttle");
2990
- const actionPath = action.get(path25);
3504
+ const actionPath = action.get(path28);
2991
3505
  if (actionPath) {
2992
3506
  actionPath.count++;
2993
3507
  return false;
2994
3508
  }
2995
3509
  let timeoutObject;
2996
3510
  const clear = () => {
2997
- const item = action.get(path25);
3511
+ const item = action.get(path28);
2998
3512
  const count = item ? item.count : 0;
2999
- action.delete(path25);
3513
+ action.delete(path28);
3000
3514
  clearTimeout(timeoutObject);
3001
3515
  if (item)
3002
3516
  clearTimeout(item.timeoutObject);
@@ -3004,7 +3518,7 @@ var FSWatcher = class extends import_events.EventEmitter {
3004
3518
  };
3005
3519
  timeoutObject = setTimeout(clear, timeout);
3006
3520
  const thr = { timeoutObject, clear, count: 0 };
3007
- action.set(path25, thr);
3521
+ action.set(path28, thr);
3008
3522
  return thr;
3009
3523
  }
3010
3524
  _incrReadyCount() {
@@ -3018,44 +3532,44 @@ var FSWatcher = class extends import_events.EventEmitter {
3018
3532
  * @param event
3019
3533
  * @param awfEmit Callback to be called when ready for event to be emitted.
3020
3534
  */
3021
- _awaitWriteFinish(path25, threshold, event, awfEmit) {
3535
+ _awaitWriteFinish(path28, threshold, event, awfEmit) {
3022
3536
  const awf = this.options.awaitWriteFinish;
3023
3537
  if (typeof awf !== "object")
3024
3538
  return;
3025
3539
  const pollInterval = awf.pollInterval;
3026
3540
  let timeoutHandler;
3027
- let fullPath = path25;
3028
- if (this.options.cwd && !sysPath2.isAbsolute(path25)) {
3029
- fullPath = sysPath2.join(this.options.cwd, path25);
3541
+ let fullPath = path28;
3542
+ if (this.options.cwd && !sysPath2.isAbsolute(path28)) {
3543
+ fullPath = sysPath2.join(this.options.cwd, path28);
3030
3544
  }
3031
3545
  const now = /* @__PURE__ */ new Date();
3032
3546
  const writes = this._pendingWrites;
3033
3547
  function awaitWriteFinishFn(prevStat) {
3034
3548
  (0, import_fs15.stat)(fullPath, (err, curStat) => {
3035
- if (err || !writes.has(path25)) {
3549
+ if (err || !writes.has(path28)) {
3036
3550
  if (err && err.code !== "ENOENT")
3037
3551
  awfEmit(err);
3038
3552
  return;
3039
3553
  }
3040
3554
  const now2 = Number(/* @__PURE__ */ new Date());
3041
3555
  if (prevStat && curStat.size !== prevStat.size) {
3042
- writes.get(path25).lastChange = now2;
3556
+ writes.get(path28).lastChange = now2;
3043
3557
  }
3044
- const pw = writes.get(path25);
3558
+ const pw = writes.get(path28);
3045
3559
  const df = now2 - pw.lastChange;
3046
3560
  if (df >= threshold) {
3047
- writes.delete(path25);
3561
+ writes.delete(path28);
3048
3562
  awfEmit(void 0, curStat);
3049
3563
  } else {
3050
3564
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
3051
3565
  }
3052
3566
  });
3053
3567
  }
3054
- if (!writes.has(path25)) {
3055
- writes.set(path25, {
3568
+ if (!writes.has(path28)) {
3569
+ writes.set(path28, {
3056
3570
  lastChange: now,
3057
3571
  cancelWait: () => {
3058
- writes.delete(path25);
3572
+ writes.delete(path28);
3059
3573
  clearTimeout(timeoutHandler);
3060
3574
  return event;
3061
3575
  }
@@ -3066,8 +3580,8 @@ var FSWatcher = class extends import_events.EventEmitter {
3066
3580
  /**
3067
3581
  * Determines whether user has asked to ignore this path.
3068
3582
  */
3069
- _isIgnored(path25, stats) {
3070
- if (this.options.atomic && DOT_RE.test(path25))
3583
+ _isIgnored(path28, stats) {
3584
+ if (this.options.atomic && DOT_RE.test(path28))
3071
3585
  return true;
3072
3586
  if (!this._userIgnored) {
3073
3587
  const { cwd } = this.options;
@@ -3077,17 +3591,17 @@ var FSWatcher = class extends import_events.EventEmitter {
3077
3591
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
3078
3592
  this._userIgnored = anymatch(list, void 0);
3079
3593
  }
3080
- return this._userIgnored(path25, stats);
3594
+ return this._userIgnored(path28, stats);
3081
3595
  }
3082
- _isntIgnored(path25, stat4) {
3083
- return !this._isIgnored(path25, stat4);
3596
+ _isntIgnored(path28, stat4) {
3597
+ return !this._isIgnored(path28, stat4);
3084
3598
  }
3085
3599
  /**
3086
3600
  * Provides a set of common helpers and properties relating to symlink handling.
3087
3601
  * @param path file or directory pattern being watched
3088
3602
  */
3089
- _getWatchHelpers(path25) {
3090
- return new WatchHelper(path25, this.options.followSymlinks, this);
3603
+ _getWatchHelpers(path28) {
3604
+ return new WatchHelper(path28, this.options.followSymlinks, this);
3091
3605
  }
3092
3606
  // Directory helpers
3093
3607
  // -----------------
@@ -3119,63 +3633,63 @@ var FSWatcher = class extends import_events.EventEmitter {
3119
3633
  * @param item base path of item/directory
3120
3634
  */
3121
3635
  _remove(directory, item, isDirectory) {
3122
- const path25 = sysPath2.join(directory, item);
3123
- const fullPath = sysPath2.resolve(path25);
3124
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path25) || this._watched.has(fullPath);
3125
- if (!this._throttle("remove", path25, 100))
3636
+ const path28 = sysPath2.join(directory, item);
3637
+ const fullPath = sysPath2.resolve(path28);
3638
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path28) || this._watched.has(fullPath);
3639
+ if (!this._throttle("remove", path28, 100))
3126
3640
  return;
3127
3641
  if (!isDirectory && this._watched.size === 1) {
3128
3642
  this.add(directory, item, true);
3129
3643
  }
3130
- const wp = this._getWatchedDir(path25);
3644
+ const wp = this._getWatchedDir(path28);
3131
3645
  const nestedDirectoryChildren = wp.getChildren();
3132
- nestedDirectoryChildren.forEach((nested) => this._remove(path25, nested));
3646
+ nestedDirectoryChildren.forEach((nested) => this._remove(path28, nested));
3133
3647
  const parent = this._getWatchedDir(directory);
3134
3648
  const wasTracked = parent.has(item);
3135
3649
  parent.remove(item);
3136
3650
  if (this._symlinkPaths.has(fullPath)) {
3137
3651
  this._symlinkPaths.delete(fullPath);
3138
3652
  }
3139
- let relPath = path25;
3653
+ let relPath = path28;
3140
3654
  if (this.options.cwd)
3141
- relPath = sysPath2.relative(this.options.cwd, path25);
3655
+ relPath = sysPath2.relative(this.options.cwd, path28);
3142
3656
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
3143
3657
  const event = this._pendingWrites.get(relPath).cancelWait();
3144
3658
  if (event === EVENTS.ADD)
3145
3659
  return;
3146
3660
  }
3147
- this._watched.delete(path25);
3661
+ this._watched.delete(path28);
3148
3662
  this._watched.delete(fullPath);
3149
3663
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
3150
- if (wasTracked && !this._isIgnored(path25))
3151
- this._emit(eventName, path25);
3152
- this._closePath(path25);
3664
+ if (wasTracked && !this._isIgnored(path28))
3665
+ this._emit(eventName, path28);
3666
+ this._closePath(path28);
3153
3667
  }
3154
3668
  /**
3155
3669
  * Closes all watchers for a path
3156
3670
  */
3157
- _closePath(path25) {
3158
- this._closeFile(path25);
3159
- const dir = sysPath2.dirname(path25);
3160
- this._getWatchedDir(dir).remove(sysPath2.basename(path25));
3671
+ _closePath(path28) {
3672
+ this._closeFile(path28);
3673
+ const dir = sysPath2.dirname(path28);
3674
+ this._getWatchedDir(dir).remove(sysPath2.basename(path28));
3161
3675
  }
3162
3676
  /**
3163
3677
  * Closes only file-specific watchers
3164
3678
  */
3165
- _closeFile(path25) {
3166
- const closers = this._closers.get(path25);
3679
+ _closeFile(path28) {
3680
+ const closers = this._closers.get(path28);
3167
3681
  if (!closers)
3168
3682
  return;
3169
3683
  closers.forEach((closer) => closer());
3170
- this._closers.delete(path25);
3684
+ this._closers.delete(path28);
3171
3685
  }
3172
- _addPathCloser(path25, closer) {
3686
+ _addPathCloser(path28, closer) {
3173
3687
  if (!closer)
3174
3688
  return;
3175
- let list = this._closers.get(path25);
3689
+ let list = this._closers.get(path28);
3176
3690
  if (!list) {
3177
3691
  list = [];
3178
- this._closers.set(path25, list);
3692
+ this._closers.set(path28, list);
3179
3693
  }
3180
3694
  list.push(closer);
3181
3695
  }
@@ -3210,54 +3724,132 @@ init_globals();
3210
3724
  function setupHotReload({
3211
3725
  app,
3212
3726
  appDir,
3727
+ projectRoot,
3213
3728
  route = "/__fw/hot",
3214
3729
  waitForBuild,
3215
3730
  onFileChange
3216
3731
  }) {
3217
3732
  const clients = /* @__PURE__ */ new Set();
3218
3733
  app.get(route, (req, res) => {
3219
- res.setHeader("Content-Type", "text/event-stream");
3220
- res.setHeader("Cache-Control", "no-cache");
3734
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
3735
+ res.setHeader("Cache-Control", "no-cache, no-transform");
3221
3736
  res.setHeader("Connection", "keep-alive");
3222
- res.flushHeaders?.();
3223
- res.write(`event: ping
3224
- data: connected
3225
-
3226
- `);
3737
+ res.setHeader("X-Accel-Buffering", "no");
3738
+ if (res.flushHeaders) {
3739
+ res.flushHeaders();
3740
+ } else {
3741
+ res.writeHead(200, {
3742
+ "Content-Type": "text/event-stream; charset=utf-8",
3743
+ "Cache-Control": "no-cache, no-transform",
3744
+ "Connection": "keep-alive",
3745
+ "X-Accel-Buffering": "no"
3746
+ });
3747
+ }
3227
3748
  clients.add(res);
3749
+ const pingMessage = "event: ping\ndata: connected\n\n";
3750
+ try {
3751
+ res.write(pingMessage, "utf-8");
3752
+ } catch (error) {
3753
+ console.error(`[hot-reload-server] \u274C Error sending ping:`, error);
3754
+ clients.delete(res);
3755
+ }
3228
3756
  req.on("close", () => {
3229
3757
  clients.delete(res);
3230
3758
  });
3759
+ req.on("aborted", () => {
3760
+ clients.delete(res);
3761
+ });
3231
3762
  });
3232
- const watcher = esm_default.watch(appDir, {
3763
+ console.log(`[hot-reload-server] \u2705 SSE endpoint registered at ${route}`);
3764
+ const resolvedProjectRoot = projectRoot ? import_path16.default.resolve(projectRoot) : import_path16.default.dirname(import_path16.default.resolve(appDir));
3765
+ const watcher = esm_default.watch(resolvedProjectRoot, {
3233
3766
  ignoreInitial: true,
3234
- ignored: ["**/node_modules/**", `**/${BUILD_FOLDER_NAME}/**`, "**/.git/**"]
3767
+ ignored: [
3768
+ "**/node_modules/**",
3769
+ `**/${BUILD_FOLDER_NAME}/**`,
3770
+ "**/.loly/**",
3771
+ // Ignore build output directory completely
3772
+ "**/.git/**",
3773
+ "**/dist/**",
3774
+ "**/build/**",
3775
+ "**/.next/**",
3776
+ "**/.cache/**",
3777
+ "**/.turbo/**",
3778
+ "**/.swc/**",
3779
+ "**/coverage/**",
3780
+ // Ignore generated files that trigger unnecessary reloads
3781
+ "**/*.map",
3782
+ // Source maps
3783
+ "**/*.log",
3784
+ // Log files
3785
+ "**/.DS_Store",
3786
+ // macOS
3787
+ "**/Thumbs.db"
3788
+ // Windows
3789
+ ],
3790
+ persistent: true,
3791
+ ignorePermissionErrors: true,
3792
+ // Only watch relevant source files (TypeScript, JavaScript, CSS)
3793
+ // Filter out JSON files in build directories
3794
+ awaitWriteFinish: {
3795
+ stabilityThreshold: 150,
3796
+ // Wait 150ms after file change to trigger event (debounce)
3797
+ pollInterval: 50
3798
+ // Check every 50ms
3799
+ }
3235
3800
  });
3801
+ let broadcastTimeout = null;
3802
+ const BROADCAST_DEBOUNCE_MS = 300;
3236
3803
  async function broadcastReload(reason, filePath) {
3804
+ const normalizedPath = import_path16.default.normalize(filePath);
3805
+ 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-")) {
3806
+ return;
3807
+ }
3237
3808
  const rel = import_path16.default.relative(appDir, filePath);
3238
3809
  console.log(`[hot-reload] ${reason}: ${rel}`);
3239
- if (onFileChange) {
3240
- try {
3241
- await onFileChange(filePath);
3242
- } catch (error) {
3243
- console.warn("[hot-reload] Error in onFileChange callback:", error);
3244
- }
3810
+ if (broadcastTimeout) {
3811
+ clearTimeout(broadcastTimeout);
3245
3812
  }
3246
- if (waitForBuild) {
3247
- try {
3248
- console.log("[hot-reload] Waiting for client bundle to finish...");
3249
- await waitForBuild();
3250
- console.log("[hot-reload] Client bundle ready, sending reload event");
3251
- } catch (error) {
3252
- console.warn("[hot-reload] Error waiting for build:", error);
3813
+ broadcastTimeout = setTimeout(async () => {
3814
+ if (onFileChange) {
3815
+ try {
3816
+ await onFileChange(filePath);
3817
+ } catch (error) {
3818
+ console.warn("[hot-reload] Error in onFileChange callback:", error);
3819
+ }
3253
3820
  }
3254
- }
3255
- for (const res of clients) {
3256
- res.write(`event: message
3821
+ if (waitForBuild) {
3822
+ try {
3823
+ console.log("[hot-reload] Waiting for client bundle to finish...");
3824
+ await waitForBuild();
3825
+ console.log("[hot-reload] Client bundle ready, sending reload event");
3826
+ } catch (error) {
3827
+ console.warn("[hot-reload] Error waiting for build:", error);
3828
+ }
3829
+ }
3830
+ const message = `event: message
3257
3831
  data: reload:${rel}
3258
3832
 
3259
- `);
3260
- }
3833
+ `;
3834
+ console.log(`[hot-reload-server] \u{1F4E4} Broadcasting reload event to ${clients.size} client(s)`);
3835
+ let sentCount = 0;
3836
+ for (const res of clients) {
3837
+ try {
3838
+ if (res.writableEnded || res.destroyed) {
3839
+ clients.delete(res);
3840
+ continue;
3841
+ }
3842
+ res.write(message, "utf-8");
3843
+ sentCount++;
3844
+ } catch (error) {
3845
+ console.error(`[hot-reload-server] \u2717 Error sending to client:`, error);
3846
+ clients.delete(res);
3847
+ }
3848
+ }
3849
+ if (sentCount > 0) {
3850
+ console.log(`[hot-reload-server] \u2705 Reload event sent to ${sentCount} client(s)`);
3851
+ }
3852
+ }, BROADCAST_DEBOUNCE_MS);
3261
3853
  }
3262
3854
  watcher.on("add", (filePath) => broadcastReload("add", filePath)).on("change", (filePath) => broadcastReload("change", filePath)).on("unlink", (filePath) => broadcastReload("unlink", filePath));
3263
3855
  }
@@ -3333,6 +3925,121 @@ function deepMerge(target, source) {
3333
3925
  }
3334
3926
  return result;
3335
3927
  }
3928
+ var ConfigValidationError = class extends Error {
3929
+ constructor(message, errors = []) {
3930
+ super(message);
3931
+ this.errors = errors;
3932
+ this.name = "ConfigValidationError";
3933
+ }
3934
+ };
3935
+ function validateConfig(config, projectRoot) {
3936
+ const errors = [];
3937
+ if (!config.directories.app || typeof config.directories.app !== "string") {
3938
+ errors.push("config.directories.app must be a non-empty string");
3939
+ } else {
3940
+ const appDir = import_path18.default.join(projectRoot, config.directories.app);
3941
+ if (!import_fs16.default.existsSync(appDir) && process.env.NODE_ENV !== "test") {
3942
+ errors.push(
3943
+ `App directory not found: ${config.directories.app}
3944
+ Expected at: ${appDir}
3945
+ \u{1F4A1} Suggestion: Create the directory or update config.directories.app`
3946
+ );
3947
+ }
3948
+ }
3949
+ if (!config.directories.build || typeof config.directories.build !== "string") {
3950
+ errors.push("config.directories.build must be a non-empty string");
3951
+ }
3952
+ if (!config.directories.static || typeof config.directories.static !== "string") {
3953
+ errors.push("config.directories.static must be a non-empty string");
3954
+ }
3955
+ const conventionKeys = ["page", "layout", "notFound", "error", "api"];
3956
+ for (const key of conventionKeys) {
3957
+ if (!config.conventions[key] || typeof config.conventions[key] !== "string") {
3958
+ errors.push(`config.conventions.${key} must be a non-empty string`);
3959
+ }
3960
+ }
3961
+ if (!["always", "never", "ignore"].includes(config.routing.trailingSlash)) {
3962
+ errors.push(
3963
+ `config.routing.trailingSlash must be 'always', 'never', or 'ignore'
3964
+ Received: ${JSON.stringify(config.routing.trailingSlash)}
3965
+ \u{1F4A1} Suggestion: Use one of the valid values: 'always' | 'never' | 'ignore'`
3966
+ );
3967
+ }
3968
+ if (typeof config.routing.caseSensitive !== "boolean") {
3969
+ errors.push("config.routing.caseSensitive must be a boolean");
3970
+ }
3971
+ if (typeof config.routing.basePath !== "string") {
3972
+ errors.push("config.routing.basePath must be a string");
3973
+ } else if (config.routing.basePath && !config.routing.basePath.startsWith("/")) {
3974
+ errors.push(
3975
+ `config.routing.basePath must start with '/' (if not empty)
3976
+ Received: ${JSON.stringify(config.routing.basePath)}
3977
+ \u{1F4A1} Suggestion: Use an empty string '' or a path starting with '/', e.g., '/api'`
3978
+ );
3979
+ }
3980
+ const validClientBundlers = ["rspack", "webpack", "vite"];
3981
+ if (!validClientBundlers.includes(config.build.clientBundler)) {
3982
+ errors.push(
3983
+ `config.build.clientBundler must be one of: ${validClientBundlers.join(", ")}
3984
+ Received: ${JSON.stringify(config.build.clientBundler)}`
3985
+ );
3986
+ }
3987
+ const validServerBundlers = ["esbuild", "tsup", "swc"];
3988
+ if (!validServerBundlers.includes(config.build.serverBundler)) {
3989
+ errors.push(
3990
+ `config.build.serverBundler must be one of: ${validServerBundlers.join(", ")}
3991
+ Received: ${JSON.stringify(config.build.serverBundler)}`
3992
+ );
3993
+ }
3994
+ if (!["cjs", "esm"].includes(config.build.outputFormat)) {
3995
+ errors.push(
3996
+ `config.build.outputFormat must be 'cjs' or 'esm'
3997
+ Received: ${JSON.stringify(config.build.outputFormat)}`
3998
+ );
3999
+ }
4000
+ const validAdapters = ["express", "fastify", "koa"];
4001
+ if (!validAdapters.includes(config.server.adapter)) {
4002
+ errors.push(
4003
+ `config.server.adapter must be one of: ${validAdapters.join(", ")}
4004
+ Received: ${JSON.stringify(config.server.adapter)}`
4005
+ );
4006
+ }
4007
+ if (typeof config.server.port !== "number" || config.server.port < 1 || config.server.port > 65535) {
4008
+ errors.push(
4009
+ `config.server.port must be a number between 1 and 65535
4010
+ Received: ${JSON.stringify(config.server.port)}`
4011
+ );
4012
+ }
4013
+ if (!config.server.host || typeof config.server.host !== "string") {
4014
+ errors.push("config.server.host must be a non-empty string");
4015
+ }
4016
+ const validFrameworks = ["react", "preact", "vue", "svelte"];
4017
+ if (!validFrameworks.includes(config.rendering.framework)) {
4018
+ errors.push(
4019
+ `config.rendering.framework must be one of: ${validFrameworks.join(", ")}
4020
+ Received: ${JSON.stringify(config.rendering.framework)}`
4021
+ );
4022
+ }
4023
+ if (typeof config.rendering.streaming !== "boolean") {
4024
+ errors.push("config.rendering.streaming must be a boolean");
4025
+ }
4026
+ if (typeof config.rendering.ssr !== "boolean") {
4027
+ errors.push("config.rendering.ssr must be a boolean");
4028
+ }
4029
+ if (typeof config.rendering.ssg !== "boolean") {
4030
+ errors.push("config.rendering.ssg must be a boolean");
4031
+ }
4032
+ if (errors.length > 0) {
4033
+ const errorMessage = [
4034
+ "\u274C Configuration validation failed:",
4035
+ "",
4036
+ ...errors.map((err, i) => `${i + 1}. ${err}`),
4037
+ "",
4038
+ "\u{1F4A1} Please check your loly.config.ts file and fix the errors above."
4039
+ ].join("\n");
4040
+ throw new ConfigValidationError(errorMessage, errors);
4041
+ }
4042
+ }
3336
4043
  function loadConfig(projectRoot) {
3337
4044
  const configFiles = [
3338
4045
  import_path18.default.join(projectRoot, "loly.config.ts"),
@@ -3340,6 +4047,7 @@ function loadConfig(projectRoot) {
3340
4047
  import_path18.default.join(projectRoot, "loly.config.json")
3341
4048
  ];
3342
4049
  let userConfig = {};
4050
+ let loadedConfigFile = null;
3343
4051
  for (const configFile of configFiles) {
3344
4052
  if (import_fs16.default.existsSync(configFile)) {
3345
4053
  try {
@@ -3353,16 +4061,31 @@ function loadConfig(projectRoot) {
3353
4061
  const mod = require(configFile);
3354
4062
  userConfig = typeof mod.default === "function" ? mod.default(process.env.NODE_ENV) : mod.default || mod.config || mod;
3355
4063
  }
4064
+ loadedConfigFile = import_path18.default.relative(projectRoot, configFile);
3356
4065
  break;
3357
4066
  } catch (error) {
3358
- console.warn(`[framework] Failed to load config from ${configFile}:`, error);
4067
+ const errorMessage = error instanceof Error ? error.message : String(error);
4068
+ throw new ConfigValidationError(
4069
+ `Failed to load configuration from ${import_path18.default.relative(projectRoot, configFile)}:
4070
+ ${errorMessage}
4071
+ \u{1F4A1} Suggestion: Check that your config file exports a valid configuration object`
4072
+ );
3359
4073
  }
3360
4074
  }
3361
4075
  }
3362
4076
  const config = deepMerge(DEFAULT_CONFIG, userConfig);
3363
- const appDir = import_path18.default.join(projectRoot, config.directories.app);
3364
- if (!import_fs16.default.existsSync(appDir) && process.env.NODE_ENV !== "test") {
3365
- console.warn(`[framework] App directory not found: ${appDir}`);
4077
+ try {
4078
+ validateConfig(config, projectRoot);
4079
+ } catch (error) {
4080
+ if (error instanceof ConfigValidationError) {
4081
+ if (loadedConfigFile) {
4082
+ error.message = `[Configuration Error in ${loadedConfigFile}]
4083
+
4084
+ ${error.message}`;
4085
+ }
4086
+ throw error;
4087
+ }
4088
+ throw error;
3366
4089
  }
3367
4090
  return config;
3368
4091
  }
@@ -3379,14 +4102,14 @@ function getStaticDir(projectRoot, config) {
3379
4102
  // modules/server/setup.ts
3380
4103
  function setupServer(app, options) {
3381
4104
  const { projectRoot, appDir, isDev, config } = options;
3382
- const routeLoader = isDev ? new FilesystemRouteLoader(appDir) : new ManifestRouteLoader(projectRoot);
4105
+ const routeLoader = isDev ? new FilesystemRouteLoader(appDir, projectRoot) : new ManifestRouteLoader(projectRoot);
3383
4106
  if (isDev) {
3384
4107
  let getRoutes2 = function() {
3385
4108
  clearAppRequireCache(appDir);
3386
- const loader = new FilesystemRouteLoader(appDir);
4109
+ sharedLoader.invalidateCache();
3387
4110
  return {
3388
- routes: loader.loadRoutes(),
3389
- apiRoutes: loader.loadApiRoutes()
4111
+ routes: sharedLoader.loadRoutes(),
4112
+ apiRoutes: sharedLoader.loadApiRoutes()
3390
4113
  };
3391
4114
  };
3392
4115
  var getRoutes = getRoutes2;
@@ -3400,19 +4123,20 @@ function setupServer(app, options) {
3400
4123
  console.log(`[hot-reload] Cleared require cache for: ${rel}`);
3401
4124
  }
3402
4125
  if (isPageFile) {
3403
- const loader = new FilesystemRouteLoader(appDir);
4126
+ const loader = new FilesystemRouteLoader(appDir, projectRoot);
3404
4127
  const newRoutes = loader.loadRoutes();
3405
4128
  writeClientRoutesManifest(newRoutes, projectRoot);
3406
4129
  console.log("[hot-reload] Client routes manifest reloaded");
3407
4130
  }
3408
4131
  };
3409
- setupHotReload({ app, appDir, waitForBuild, onFileChange });
4132
+ setupHotReload({ app, appDir, projectRoot, waitForBuild, onFileChange });
3410
4133
  app.use("/static", import_express.default.static(outDir));
3411
4134
  const routes = routeLoader.loadRoutes();
3412
4135
  const wssRoutes = routeLoader.loadWssRoutes();
3413
4136
  const notFoundPage = routeLoader.loadNotFoundRoute();
3414
4137
  const errorPage = routeLoader.loadErrorRoute();
3415
4138
  writeClientRoutesManifest(routes, projectRoot);
4139
+ const sharedLoader = new FilesystemRouteLoader(appDir, projectRoot);
3416
4140
  return {
3417
4141
  routes,
3418
4142
  wssRoutes,
@@ -3500,90 +4224,10 @@ function sanitizeQuery(query) {
3500
4224
 
3501
4225
  // modules/server/middleware/rate-limit.ts
3502
4226
  var import_express_rate_limit = __toESM(require("express-rate-limit"));
3503
- function createRateLimiter(config = {}) {
3504
- const {
3505
- windowMs = 15 * 60 * 1e3,
3506
- // 15 minutes
3507
- max = 100,
3508
- // limit each IP to 100 requests per windowMs
3509
- message = "Too many requests from this IP, please try again later.",
3510
- standardHeaders = true,
3511
- legacyHeaders = false,
3512
- skipSuccessfulRequests = false,
3513
- skipFailedRequests = false
3514
- } = config;
3515
- return (0, import_express_rate_limit.default)({
3516
- windowMs,
3517
- max,
3518
- message: {
3519
- error: message
3520
- },
3521
- standardHeaders,
3522
- legacyHeaders,
3523
- skipSuccessfulRequests,
3524
- skipFailedRequests
3525
- });
3526
- }
3527
- var defaultRateLimiter = createRateLimiter({
3528
- windowMs: 15 * 60 * 1e3,
3529
- // 15 minutes
3530
- max: 100,
3531
- message: "Too many requests from this IP, please try again later."
3532
- });
3533
- var strictRateLimiter = createRateLimiter({
3534
- windowMs: 15 * 60 * 1e3,
3535
- // 15 minutes
3536
- max: 5,
3537
- message: "Too many authentication attempts, please try again later."
3538
- });
3539
- var lenientRateLimiter = createRateLimiter({
3540
- windowMs: 15 * 60 * 1e3,
3541
- // 15 minutes
3542
- max: 200,
3543
- message: "Too many requests from this IP, please try again later."
3544
- });
3545
-
3546
- // modules/server/middleware/auto-rate-limit.ts
3547
- function matchesStrictPattern(path25, patterns) {
3548
- for (const pattern of patterns) {
3549
- const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
3550
- const regex = new RegExp(`^${regexPattern}$`);
3551
- if (regex.test(path25)) {
3552
- return true;
3553
- }
3554
- }
3555
- return false;
3556
- }
3557
- function isRateLimiter(mw) {
3558
- if (!mw) return false;
3559
- if (mw === strictRateLimiter || mw === defaultRateLimiter || mw === lenientRateLimiter) {
3560
- return true;
3561
- }
3562
- if (typeof mw === "function" && mw.name && mw.name.includes("rateLimit")) {
3563
- return true;
3564
- }
3565
- if (mw && typeof mw === "function" && mw.skip || mw.resetKey) {
3566
- return true;
3567
- }
3568
- return false;
3569
- }
3570
- function getAutoRateLimiter(route, strictPatterns = []) {
3571
- const hasRateLimiter = route.middlewares?.some(isRateLimiter) || Object.values(route.methodMiddlewares || {}).some(
3572
- (mws) => mws?.some(isRateLimiter)
3573
- );
3574
- if (hasRateLimiter) {
3575
- return null;
3576
- }
3577
- if (strictPatterns.length > 0 && matchesStrictPattern(route.pattern, strictPatterns)) {
3578
- console.log(`[rate-limit] Applying strict rate limiter to route: ${route.pattern}`);
3579
- return strictRateLimiter;
3580
- }
3581
- return null;
3582
- }
3583
-
3584
- // modules/logger/index.ts
3585
- var import_pino = __toESM(require("pino"));
3586
- function createLogger(options = {}) {
4227
+
4228
+ // modules/logger/index.ts
4229
+ var import_pino = __toESM(require("pino"));
4230
+ function createLogger(options = {}) {
3587
4231
  const {
3588
4232
  level = process.env.LOG_LEVEL || (process.env.NODE_ENV === "development" ? "debug" : "info"),
3589
4233
  enabled = process.env.LOG_ENABLED !== "false",
@@ -3639,8 +4283,8 @@ function resetLogger() {
3639
4283
  loggerInstance = null;
3640
4284
  }
3641
4285
  var Logger = class _Logger {
3642
- constructor(logger4, context = {}) {
3643
- this.pino = logger4 || getLogger();
4286
+ constructor(logger5, context = {}) {
4287
+ this.pino = logger5 || getLogger();
3644
4288
  this.context = context;
3645
4289
  }
3646
4290
  /**
@@ -3717,12 +4361,12 @@ var DEFAULT_IGNORED_PATHS = [
3717
4361
  /^\/sockjs-node/
3718
4362
  // Hot reload websocket
3719
4363
  ];
3720
- function shouldIgnorePath(path25, ignoredPaths) {
4364
+ function shouldIgnorePath(path28, ignoredPaths) {
3721
4365
  return ignoredPaths.some((pattern) => {
3722
4366
  if (typeof pattern === "string") {
3723
- return path25 === pattern || path25.startsWith(pattern);
4367
+ return path28 === pattern || path28.startsWith(pattern);
3724
4368
  }
3725
- return pattern.test(path25);
4369
+ return pattern.test(path28);
3726
4370
  });
3727
4371
  }
3728
4372
  function requestLoggerMiddleware(options = {}) {
@@ -3773,6 +4417,151 @@ function getRequestLogger(req) {
3773
4417
  return req.logger || logger.child({ requestId: "unknown" });
3774
4418
  }
3775
4419
 
4420
+ // modules/server/middleware/rate-limit.ts
4421
+ var logger2 = createModuleLogger("rate-limit");
4422
+ function validateRateLimitConfig(config) {
4423
+ if (config.windowMs !== void 0 && (config.windowMs < 1e3 || !Number.isInteger(config.windowMs))) {
4424
+ throw new Error(
4425
+ `Invalid rateLimit.windowMs: ${config.windowMs}. Must be an integer >= 1000 (milliseconds)`
4426
+ );
4427
+ }
4428
+ if (config.max !== void 0 && (config.max < 1 || !Number.isInteger(config.max))) {
4429
+ throw new Error(
4430
+ `Invalid rateLimit.max: ${config.max}. Must be an integer >= 1`
4431
+ );
4432
+ }
4433
+ }
4434
+ function createRateLimiter(config = {}) {
4435
+ validateRateLimitConfig(config);
4436
+ const {
4437
+ windowMs = 15 * 60 * 1e3,
4438
+ // 15 minutes
4439
+ max = 100,
4440
+ // limit each IP to 100 requests per windowMs
4441
+ message = "Too many requests from this IP, please try again later.",
4442
+ standardHeaders = true,
4443
+ legacyHeaders = false,
4444
+ skipSuccessfulRequests = false,
4445
+ skipFailedRequests = false,
4446
+ keyGenerator,
4447
+ skip
4448
+ } = config;
4449
+ const limiter = (0, import_express_rate_limit.default)({
4450
+ windowMs,
4451
+ max,
4452
+ message: {
4453
+ error: message,
4454
+ retryAfter: Math.ceil(windowMs / 1e3)
4455
+ // seconds until retry
4456
+ },
4457
+ standardHeaders,
4458
+ legacyHeaders,
4459
+ skipSuccessfulRequests,
4460
+ skipFailedRequests,
4461
+ keyGenerator,
4462
+ skip
4463
+ });
4464
+ const wrappedLimiter = (req, res, next) => {
4465
+ limiter(req, res, (err) => {
4466
+ if (err && res.statusCode === 429) {
4467
+ const ip = req.ip || req.connection?.remoteAddress || "unknown";
4468
+ logger2.warn("Rate limit exceeded", {
4469
+ ip,
4470
+ path: req.path,
4471
+ method: req.method,
4472
+ limit: max,
4473
+ windowMs,
4474
+ retryAfter: Math.ceil(windowMs / 1e3)
4475
+ });
4476
+ }
4477
+ if (err) {
4478
+ return next(err);
4479
+ }
4480
+ next();
4481
+ });
4482
+ };
4483
+ Object.setPrototypeOf(wrappedLimiter, limiter);
4484
+ Object.assign(wrappedLimiter, limiter);
4485
+ return wrappedLimiter;
4486
+ }
4487
+ var defaultRateLimiter = createRateLimiter({
4488
+ windowMs: 15 * 60 * 1e3,
4489
+ // 15 minutes
4490
+ max: 100,
4491
+ message: "Too many requests from this IP, please try again later."
4492
+ });
4493
+ var strictRateLimiter = createRateLimiter({
4494
+ windowMs: 15 * 60 * 1e3,
4495
+ // 15 minutes
4496
+ max: 5,
4497
+ message: "Too many authentication attempts, please try again later."
4498
+ });
4499
+ var lenientRateLimiter = createRateLimiter({
4500
+ windowMs: 15 * 60 * 1e3,
4501
+ // 15 minutes
4502
+ max: 200,
4503
+ message: "Too many requests from this IP, please try again later."
4504
+ });
4505
+ function createRateLimiterFromConfig(config, useApiMax = false) {
4506
+ if (!config) return null;
4507
+ const max = useApiMax ? config.apiMax ?? config.max ?? 100 : config.max ?? 100;
4508
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
4509
+ return createRateLimiter({
4510
+ windowMs,
4511
+ max,
4512
+ message: `Too many requests from this IP, please try again after ${Math.ceil(windowMs / 1e3)} seconds.`
4513
+ });
4514
+ }
4515
+ function createStrictRateLimiterFromConfig(config) {
4516
+ if (!config || config.strictMax === void 0) {
4517
+ return strictRateLimiter;
4518
+ }
4519
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
4520
+ return createRateLimiter({
4521
+ windowMs,
4522
+ max: config.strictMax,
4523
+ message: `Too many authentication attempts, please try again after ${Math.ceil(windowMs / 1e3)} seconds.`
4524
+ });
4525
+ }
4526
+
4527
+ // modules/server/middleware/auto-rate-limit.ts
4528
+ function matchesStrictPattern(path28, patterns) {
4529
+ for (const pattern of patterns) {
4530
+ const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
4531
+ const regex = new RegExp(`^${regexPattern}$`);
4532
+ if (regex.test(path28)) {
4533
+ return true;
4534
+ }
4535
+ }
4536
+ return false;
4537
+ }
4538
+ function isRateLimiter(mw) {
4539
+ if (!mw) return false;
4540
+ if (mw === strictRateLimiter || mw === defaultRateLimiter || mw === lenientRateLimiter) {
4541
+ return true;
4542
+ }
4543
+ if (typeof mw === "function" && mw.name && mw.name.includes("rateLimit")) {
4544
+ return true;
4545
+ }
4546
+ if (mw && typeof mw === "function" && mw.skip || mw.resetKey) {
4547
+ return true;
4548
+ }
4549
+ return false;
4550
+ }
4551
+ function getAutoRateLimiter(route, strictPatterns = [], rateLimitConfig) {
4552
+ const hasRateLimiter = route.middlewares?.some(isRateLimiter) || Object.values(route.methodMiddlewares || {}).some(
4553
+ (mws) => mws?.some(isRateLimiter)
4554
+ );
4555
+ if (hasRateLimiter) {
4556
+ return null;
4557
+ }
4558
+ if (strictPatterns.length > 0 && matchesStrictPattern(route.pattern, strictPatterns)) {
4559
+ const limiter = rateLimitConfig ? createStrictRateLimiterFromConfig(rateLimitConfig) : strictRateLimiter;
4560
+ return limiter;
4561
+ }
4562
+ return null;
4563
+ }
4564
+
3776
4565
  // modules/server/handlers/api.ts
3777
4566
  async function handleApiRequest(options) {
3778
4567
  const { apiRoutes, urlPath, req, res, env = "dev" } = options;
@@ -3804,7 +4593,8 @@ async function handleApiRequest(options) {
3804
4593
  try {
3805
4594
  const autoRateLimiter = getAutoRateLimiter(
3806
4595
  route,
3807
- options.strictRateLimitPatterns
4596
+ options.strictRateLimitPatterns,
4597
+ options.rateLimitConfig
3808
4598
  );
3809
4599
  const reqLogger = getRequestLogger(req);
3810
4600
  if (autoRateLimiter) {
@@ -3816,26 +4606,46 @@ async function handleApiRequest(options) {
3816
4606
  const globalMws = route.middlewares ?? [];
3817
4607
  const perMethodMws = route.methodMiddlewares?.[method] ?? [];
3818
4608
  const chain = autoRateLimiter ? [autoRateLimiter, ...globalMws, ...perMethodMws] : [...globalMws, ...perMethodMws];
3819
- for (const mw of chain) {
3820
- const isExpressRateLimit = mw && typeof mw === "function" && (mw.skip || mw.resetKey || mw.name?.includes("rateLimit"));
3821
- if (isExpressRateLimit) {
3822
- await new Promise((resolve3, reject) => {
3823
- const next = (err) => {
3824
- if (err) reject(err);
3825
- else resolve3();
3826
- };
3827
- try {
3828
- const result = mw(req, res, next);
3829
- if (result && typeof result.then === "function") {
3830
- result.then(() => resolve3()).catch(reject);
4609
+ for (let i = 0; i < chain.length; i++) {
4610
+ const mw = chain[i];
4611
+ if (typeof mw !== "function") {
4612
+ reqLogger.warn("Invalid middleware in chain", {
4613
+ route: route.pattern,
4614
+ method,
4615
+ middlewareIndex: i,
4616
+ middlewareType: typeof mw
4617
+ });
4618
+ continue;
4619
+ }
4620
+ try {
4621
+ const isExpressRateLimit = mw.skip || mw.resetKey || mw.name?.includes("rateLimit");
4622
+ if (isExpressRateLimit) {
4623
+ await new Promise((resolve3, reject) => {
4624
+ const next = (err) => {
4625
+ if (err) reject(err);
4626
+ else resolve3();
4627
+ };
4628
+ try {
4629
+ const result = mw(req, res, next);
4630
+ if (result && typeof result.then === "function") {
4631
+ result.then(() => resolve3()).catch(reject);
4632
+ }
4633
+ } catch (err) {
4634
+ reject(err);
3831
4635
  }
3832
- } catch (err) {
3833
- reject(err);
3834
- }
4636
+ });
4637
+ } else {
4638
+ await Promise.resolve(mw(ctx, async () => {
4639
+ }));
4640
+ }
4641
+ } catch (error) {
4642
+ reqLogger.error("API middleware failed", error instanceof Error ? error : new Error(String(error)), {
4643
+ route: route.pattern,
4644
+ method,
4645
+ middlewareIndex: i,
4646
+ middlewareName: mw.name || "anonymous"
3835
4647
  });
3836
- } else {
3837
- await Promise.resolve(mw(ctx, async () => {
3838
- }));
4648
+ throw error;
3839
4649
  }
3840
4650
  if (res.headersSent) {
3841
4651
  return;
@@ -3886,34 +4696,260 @@ function createDocumentTree(options) {
3886
4696
  titleFallback,
3887
4697
  descriptionFallback,
3888
4698
  chunkHref,
4699
+ entrypointFiles = [],
3889
4700
  theme,
3890
4701
  clientJsPath = "/static/client.js",
3891
4702
  clientCssPath = "/static/client.css",
3892
- nonce
4703
+ nonce,
4704
+ includeInlineScripts = true
4705
+ // Default true - scripts inline in body for both SSR and SSG
3893
4706
  } = options;
3894
- const metaObj = meta ?? {};
3895
- const title = metaObj.title ?? titleFallback ?? "My Framework Dev";
3896
- const lang = metaObj.lang ?? "en";
3897
- const description = metaObj.description ?? descriptionFallback ?? "Demo Loly framework";
4707
+ const metaObj = meta ?? null;
4708
+ const title = metaObj?.title ?? titleFallback ?? "My Framework Dev";
4709
+ const lang = metaObj?.lang ?? "en";
4710
+ const description = metaObj?.description ?? descriptionFallback ?? "Demo Loly framework";
3898
4711
  const extraMetaTags = [];
4712
+ const linkTags = [];
3899
4713
  if (description) {
3900
4714
  extraMetaTags.push(
3901
4715
  import_react.default.createElement("meta", {
4716
+ key: "meta-description",
3902
4717
  name: "description",
3903
4718
  content: description
3904
4719
  })
3905
4720
  );
3906
4721
  }
3907
- if (Array.isArray(metaObj.metaTags)) {
3908
- for (const tag of metaObj.metaTags) {
4722
+ if (metaObj?.robots) {
4723
+ extraMetaTags.push(
4724
+ import_react.default.createElement("meta", {
4725
+ key: "meta-robots",
4726
+ name: "robots",
4727
+ content: metaObj.robots
4728
+ })
4729
+ );
4730
+ }
4731
+ if (metaObj?.themeColor) {
4732
+ extraMetaTags.push(
4733
+ import_react.default.createElement("meta", {
4734
+ key: "meta-theme-color",
4735
+ name: "theme-color",
4736
+ content: metaObj.themeColor
4737
+ })
4738
+ );
4739
+ }
4740
+ if (metaObj?.viewport) {
4741
+ extraMetaTags.push(
4742
+ import_react.default.createElement("meta", {
4743
+ key: "meta-viewport",
4744
+ name: "viewport",
4745
+ content: metaObj.viewport
4746
+ })
4747
+ );
4748
+ }
4749
+ if (metaObj?.canonical) {
4750
+ linkTags.push(
4751
+ import_react.default.createElement("link", {
4752
+ key: "link-canonical",
4753
+ rel: "canonical",
4754
+ href: metaObj.canonical
4755
+ })
4756
+ );
4757
+ }
4758
+ if (metaObj?.openGraph) {
4759
+ const og = metaObj.openGraph;
4760
+ if (og.title) {
4761
+ extraMetaTags.push(
4762
+ import_react.default.createElement("meta", {
4763
+ key: "og-title",
4764
+ property: "og:title",
4765
+ content: og.title
4766
+ })
4767
+ );
4768
+ }
4769
+ if (og.description) {
4770
+ extraMetaTags.push(
4771
+ import_react.default.createElement("meta", {
4772
+ key: "og-description",
4773
+ property: "og:description",
4774
+ content: og.description
4775
+ })
4776
+ );
4777
+ }
4778
+ if (og.type) {
4779
+ extraMetaTags.push(
4780
+ import_react.default.createElement("meta", {
4781
+ key: "og-type",
4782
+ property: "og:type",
4783
+ content: og.type
4784
+ })
4785
+ );
4786
+ }
4787
+ if (og.url) {
4788
+ extraMetaTags.push(
4789
+ import_react.default.createElement("meta", {
4790
+ key: "og-url",
4791
+ property: "og:url",
4792
+ content: og.url
4793
+ })
4794
+ );
4795
+ }
4796
+ if (og.image) {
4797
+ if (typeof og.image === "string") {
4798
+ extraMetaTags.push(
4799
+ import_react.default.createElement("meta", {
4800
+ key: "og-image",
4801
+ property: "og:image",
4802
+ content: og.image
4803
+ })
4804
+ );
4805
+ } else {
4806
+ extraMetaTags.push(
4807
+ import_react.default.createElement("meta", {
4808
+ key: "og-image",
4809
+ property: "og:image",
4810
+ content: og.image.url
4811
+ })
4812
+ );
4813
+ if (og.image.width) {
4814
+ extraMetaTags.push(
4815
+ import_react.default.createElement("meta", {
4816
+ key: "og-image-width",
4817
+ property: "og:image:width",
4818
+ content: String(og.image.width)
4819
+ })
4820
+ );
4821
+ }
4822
+ if (og.image.height) {
4823
+ extraMetaTags.push(
4824
+ import_react.default.createElement("meta", {
4825
+ key: "og-image-height",
4826
+ property: "og:image:height",
4827
+ content: String(og.image.height)
4828
+ })
4829
+ );
4830
+ }
4831
+ if (og.image.alt) {
4832
+ extraMetaTags.push(
4833
+ import_react.default.createElement("meta", {
4834
+ key: "og-image-alt",
4835
+ property: "og:image:alt",
4836
+ content: og.image.alt
4837
+ })
4838
+ );
4839
+ }
4840
+ }
4841
+ }
4842
+ if (og.siteName) {
4843
+ extraMetaTags.push(
4844
+ import_react.default.createElement("meta", {
4845
+ key: "og-site-name",
4846
+ property: "og:site_name",
4847
+ content: og.siteName
4848
+ })
4849
+ );
4850
+ }
4851
+ if (og.locale) {
4852
+ extraMetaTags.push(
4853
+ import_react.default.createElement("meta", {
4854
+ key: "og-locale",
4855
+ property: "og:locale",
4856
+ content: og.locale
4857
+ })
4858
+ );
4859
+ }
4860
+ }
4861
+ if (metaObj?.twitter) {
4862
+ const twitter = metaObj.twitter;
4863
+ if (twitter.card) {
4864
+ extraMetaTags.push(
4865
+ import_react.default.createElement("meta", {
4866
+ key: "twitter-card",
4867
+ name: "twitter:card",
4868
+ content: twitter.card
4869
+ })
4870
+ );
4871
+ }
4872
+ if (twitter.title) {
4873
+ extraMetaTags.push(
4874
+ import_react.default.createElement("meta", {
4875
+ key: "twitter-title",
4876
+ name: "twitter:title",
4877
+ content: twitter.title
4878
+ })
4879
+ );
4880
+ }
4881
+ if (twitter.description) {
4882
+ extraMetaTags.push(
4883
+ import_react.default.createElement("meta", {
4884
+ key: "twitter-description",
4885
+ name: "twitter:description",
4886
+ content: twitter.description
4887
+ })
4888
+ );
4889
+ }
4890
+ if (twitter.image) {
4891
+ extraMetaTags.push(
4892
+ import_react.default.createElement("meta", {
4893
+ key: "twitter-image",
4894
+ name: "twitter:image",
4895
+ content: twitter.image
4896
+ })
4897
+ );
4898
+ }
4899
+ if (twitter.imageAlt) {
4900
+ extraMetaTags.push(
4901
+ import_react.default.createElement("meta", {
4902
+ key: "twitter-image-alt",
4903
+ name: "twitter:image:alt",
4904
+ content: twitter.imageAlt
4905
+ })
4906
+ );
4907
+ }
4908
+ if (twitter.site) {
4909
+ extraMetaTags.push(
4910
+ import_react.default.createElement("meta", {
4911
+ key: "twitter-site",
4912
+ name: "twitter:site",
4913
+ content: twitter.site
4914
+ })
4915
+ );
4916
+ }
4917
+ if (twitter.creator) {
4918
+ extraMetaTags.push(
4919
+ import_react.default.createElement("meta", {
4920
+ key: "twitter-creator",
4921
+ name: "twitter:creator",
4922
+ content: twitter.creator
4923
+ })
4924
+ );
4925
+ }
4926
+ }
4927
+ if (metaObj?.metaTags && Array.isArray(metaObj.metaTags)) {
4928
+ metaObj.metaTags.forEach((tag, index) => {
3909
4929
  extraMetaTags.push(
3910
4930
  import_react.default.createElement("meta", {
4931
+ key: `meta-custom-${index}`,
3911
4932
  name: tag.name,
3912
4933
  property: tag.property,
4934
+ httpEquiv: tag.httpEquiv,
3913
4935
  content: tag.content
3914
4936
  })
3915
4937
  );
3916
- }
4938
+ });
4939
+ }
4940
+ if (metaObj?.links && Array.isArray(metaObj.links)) {
4941
+ metaObj.links.forEach((link, index) => {
4942
+ linkTags.push(
4943
+ import_react.default.createElement("link", {
4944
+ key: `link-custom-${index}`,
4945
+ rel: link.rel,
4946
+ href: link.href,
4947
+ as: link.as,
4948
+ crossOrigin: link.crossorigin,
4949
+ type: link.type
4950
+ })
4951
+ );
4952
+ });
3917
4953
  }
3918
4954
  const serialized = JSON.stringify({
3919
4955
  ...initialData,
@@ -3922,6 +4958,27 @@ function createDocumentTree(options) {
3922
4958
  const routerSerialized = JSON.stringify({
3923
4959
  ...routerData
3924
4960
  });
4961
+ const bodyChildren = [
4962
+ import_react.default.createElement("div", { id: APP_CONTAINER_ID }, appTree)
4963
+ ];
4964
+ if (includeInlineScripts) {
4965
+ bodyChildren.push(
4966
+ import_react.default.createElement("script", {
4967
+ key: "initial-data",
4968
+ nonce,
4969
+ dangerouslySetInnerHTML: {
4970
+ __html: `window.${WINDOW_DATA_KEY} = ${serialized};`
4971
+ }
4972
+ }),
4973
+ import_react.default.createElement("script", {
4974
+ key: "router-data",
4975
+ nonce,
4976
+ dangerouslySetInnerHTML: {
4977
+ __html: `window.${ROUTER_DATA_KEY} = ${routerSerialized};`
4978
+ }
4979
+ })
4980
+ );
4981
+ }
3925
4982
  const documentTree = import_react.default.createElement(
3926
4983
  "html",
3927
4984
  { lang },
@@ -3930,13 +4987,25 @@ function createDocumentTree(options) {
3930
4987
  null,
3931
4988
  import_react.default.createElement("meta", { charSet: "utf-8" }),
3932
4989
  import_react.default.createElement("title", null, title),
4990
+ // Viewport: use custom if provided, otherwise default
3933
4991
  import_react.default.createElement("meta", {
3934
4992
  name: "viewport",
3935
- content: "width=device-width, initial-scale=1"
4993
+ content: metaObj?.viewport ?? "width=device-width, initial-scale=1"
3936
4994
  }),
3937
4995
  ...extraMetaTags,
4996
+ ...linkTags,
4997
+ ...entrypointFiles.length > 0 ? entrypointFiles.slice(0, -1).map(
4998
+ (file) => import_react.default.createElement("link", {
4999
+ key: `preload-${file}`,
5000
+ rel: "preload",
5001
+ href: file,
5002
+ as: "script"
5003
+ })
5004
+ ) : [],
5005
+ // Preload route-specific chunk if available
3938
5006
  chunkHref && import_react.default.createElement("link", {
3939
- rel: "modulepreload",
5007
+ key: `preload-${chunkHref}`,
5008
+ rel: "preload",
3940
5009
  href: chunkHref,
3941
5010
  as: "script"
3942
5011
  }),
@@ -3949,33 +5018,34 @@ function createDocumentTree(options) {
3949
5018
  rel: "stylesheet",
3950
5019
  href: clientCssPath
3951
5020
  }),
3952
- import_react.default.createElement("script", {
3953
- src: clientJsPath,
3954
- defer: true
3955
- })
5021
+ ...entrypointFiles.length > 0 ? entrypointFiles.map(
5022
+ (file) => import_react.default.createElement("script", {
5023
+ key: file,
5024
+ src: file,
5025
+ defer: true,
5026
+ nonce
5027
+ // CSP nonce for external scripts
5028
+ })
5029
+ ) : [
5030
+ import_react.default.createElement("script", {
5031
+ key: "client",
5032
+ src: clientJsPath,
5033
+ defer: true,
5034
+ nonce
5035
+ // CSP nonce for external scripts
5036
+ })
5037
+ ]
3956
5038
  ),
3957
5039
  import_react.default.createElement(
3958
5040
  "body",
3959
5041
  {
3960
5042
  style: { margin: 0 },
3961
- className: [initialData.className, theme].filter(Boolean).join(" "),
5043
+ className: [initialData.className || "", theme].filter(Boolean).join(" "),
3962
5044
  suppressHydrationWarning: true
3963
5045
  // Allow theme class to differ between server and client initially
3964
5046
  },
3965
- import_react.default.createElement("div", { id: APP_CONTAINER_ID }, appTree)
3966
- ),
3967
- import_react.default.createElement("script", {
3968
- nonce,
3969
- dangerouslySetInnerHTML: {
3970
- __html: `window.${WINDOW_DATA_KEY} = ${serialized};`
3971
- }
3972
- }),
3973
- import_react.default.createElement("script", {
3974
- nonce,
3975
- dangerouslySetInnerHTML: {
3976
- __html: `window.${ROUTER_DATA_KEY} = ${routerSerialized};`
3977
- }
3978
- })
5047
+ ...bodyChildren
5048
+ )
3979
5049
  );
3980
5050
  return documentTree;
3981
5051
  }
@@ -3991,7 +5061,7 @@ function buildInitialData(urlPath, params, loaderResult) {
3991
5061
  params,
3992
5062
  props,
3993
5063
  metadata: loaderResult.metadata ?? null,
3994
- className: loaderResult.className ?? "",
5064
+ className: loaderResult.className,
3995
5065
  error: false,
3996
5066
  notFound: false
3997
5067
  };
@@ -4007,24 +5077,83 @@ var buildRouterData = (req) => {
4007
5077
  };
4008
5078
 
4009
5079
  // modules/server/handlers/middleware.ts
5080
+ var import_path20 = __toESM(require("path"));
4010
5081
  async function runRouteMiddlewares(route, ctx) {
4011
- for (const mw of route.middlewares) {
4012
- await Promise.resolve(
4013
- mw(ctx, async () => {
4014
- })
4015
- );
5082
+ for (let i = 0; i < route.middlewares.length; i++) {
5083
+ const mw = route.middlewares[i];
5084
+ try {
5085
+ await Promise.resolve(
5086
+ mw(ctx, async () => {
5087
+ })
5088
+ );
5089
+ } catch (error) {
5090
+ const reqLogger = getRequestLogger(ctx.req);
5091
+ const relativePath = route.pageFile ? import_path20.default.relative(process.cwd(), route.pageFile) : route.pattern;
5092
+ reqLogger.error("Route middleware failed", error instanceof Error ? error : new Error(String(error)), {
5093
+ route: route.pattern,
5094
+ middlewareIndex: i,
5095
+ pageFile: relativePath
5096
+ });
5097
+ throw error;
5098
+ }
4016
5099
  if (ctx.res.headersSent) {
4017
5100
  return;
4018
5101
  }
4019
5102
  }
4020
5103
  }
4021
5104
 
4022
- // modules/server/handlers/loader.ts
4023
- async function runRouteLoader(route, ctx) {
5105
+ // modules/server/handlers/server-hook.ts
5106
+ var import_path21 = __toESM(require("path"));
5107
+ function createServerHookErrorMessage(error, hookType, routePattern, filePath) {
5108
+ const errorMessage = error instanceof Error ? error.message : String(error);
5109
+ const errorStack = error instanceof Error ? error.stack : void 0;
5110
+ let message = `[${hookType.toUpperCase()} SERVER HOOK ERROR]
5111
+ `;
5112
+ message += `Route: ${routePattern}
5113
+ `;
5114
+ if (filePath) {
5115
+ const relativePath = import_path21.default.relative(process.cwd(), filePath);
5116
+ message += `File: ${relativePath}
5117
+ `;
5118
+ }
5119
+ message += `Error: ${errorMessage}
5120
+ `;
5121
+ if (errorMessage.includes("Cannot find module")) {
5122
+ message += `
5123
+ \u{1F4A1} Suggestion: Check that all imports in your ${hookType}.server.hook.ts are correct.
5124
+ `;
5125
+ } else if (errorMessage.includes("is not defined") || errorMessage.includes("Cannot read property")) {
5126
+ message += `
5127
+ \u{1F4A1} Suggestion: Verify that all variables and properties exist in your ${hookType}.server.hook.ts.
5128
+ `;
5129
+ } else if (errorMessage.includes("async") || errorMessage.includes("await")) {
5130
+ message += `
5131
+ \u{1F4A1} Suggestion: Make sure getServerSideProps is an async function and all promises are awaited.
5132
+ `;
5133
+ }
5134
+ if (errorStack) {
5135
+ message += `
5136
+ Stack trace:
5137
+ ${errorStack}`;
5138
+ }
5139
+ return message;
5140
+ }
5141
+ async function runRouteServerHook(route, ctx) {
4024
5142
  if (!route.loader) {
4025
5143
  return { props: {} };
4026
5144
  }
4027
- return await route.loader(ctx);
5145
+ try {
5146
+ return await route.loader(ctx);
5147
+ } catch (error) {
5148
+ const detailedError = new Error(
5149
+ createServerHookErrorMessage(error, "page", route.pattern, route.pageFile)
5150
+ );
5151
+ if (error instanceof Error && error.stack) {
5152
+ detailedError.stack = error.stack;
5153
+ }
5154
+ detailedError.originalError = error;
5155
+ throw detailedError;
5156
+ }
4028
5157
  }
4029
5158
 
4030
5159
  // modules/server/handlers/response.ts
@@ -4065,26 +5194,26 @@ function handleNotFound(res, urlPath) {
4065
5194
 
4066
5195
  // modules/server/handlers/ssg.ts
4067
5196
  var import_fs17 = __toESM(require("fs"));
4068
- var import_path20 = __toESM(require("path"));
4069
- var logger2 = createModuleLogger("ssg");
5197
+ var import_path22 = __toESM(require("path"));
5198
+ var logger3 = createModuleLogger("ssg");
4070
5199
  function getSsgDirForPath(baseDir, urlPath) {
4071
5200
  const clean = urlPath === "/" ? "" : urlPath.replace(/^\/+/, "");
4072
- return import_path20.default.join(baseDir, clean);
5201
+ return import_path22.default.join(baseDir, clean);
4073
5202
  }
4074
5203
  function getSsgHtmlPath(baseDir, urlPath) {
4075
5204
  const dir = getSsgDirForPath(baseDir, urlPath);
4076
- return import_path20.default.join(dir, "index.html");
5205
+ return import_path22.default.join(dir, "index.html");
4077
5206
  }
4078
5207
  function getSsgDataPath(baseDir, urlPath) {
4079
5208
  const dir = getSsgDirForPath(baseDir, urlPath);
4080
- return import_path20.default.join(dir, "data.json");
5209
+ return import_path22.default.join(dir, "data.json");
4081
5210
  }
4082
5211
  function tryServeSsgHtml(res, ssgOutDir, urlPath) {
4083
5212
  const ssgHtmlPath = getSsgHtmlPath(ssgOutDir, urlPath);
4084
5213
  if (!import_fs17.default.existsSync(ssgHtmlPath)) {
4085
5214
  return false;
4086
5215
  }
4087
- logger2.info("Serving SSG HTML", { urlPath, ssgHtmlPath });
5216
+ logger3.info("Serving SSG HTML", { urlPath, ssgHtmlPath });
4088
5217
  res.setHeader(
4089
5218
  "Content-Security-Policy",
4090
5219
  "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';"
@@ -4106,13 +5235,43 @@ function tryServeSsgData(res, ssgOutDir, urlPath) {
4106
5235
  res.status(200).end(raw);
4107
5236
  return true;
4108
5237
  } catch (err) {
4109
- logger2.error("Error reading SSG data", err, { urlPath, ssgDataPath });
5238
+ logger3.error("Error reading SSG data", err, { urlPath, ssgDataPath });
4110
5239
  return false;
4111
5240
  }
4112
5241
  }
4113
5242
 
4114
5243
  // modules/server/handlers/pages.ts
4115
5244
  init_globals();
5245
+ var import_path23 = __toESM(require("path"));
5246
+ function mergeMetadata(base, override) {
5247
+ if (!base && !override) return null;
5248
+ if (!base) return override;
5249
+ if (!override) return base;
5250
+ return {
5251
+ // Simple fields: override wins
5252
+ title: override.title ?? base.title,
5253
+ description: override.description ?? base.description,
5254
+ lang: override.lang ?? base.lang,
5255
+ canonical: override.canonical ?? base.canonical,
5256
+ robots: override.robots ?? base.robots,
5257
+ themeColor: override.themeColor ?? base.themeColor,
5258
+ viewport: override.viewport ?? base.viewport,
5259
+ // Nested objects: shallow merge (override wins for each field)
5260
+ openGraph: override.openGraph ? {
5261
+ ...base.openGraph,
5262
+ ...override.openGraph,
5263
+ // For image, if override has image, use it entirely (don't merge)
5264
+ image: override.openGraph.image ?? base.openGraph?.image
5265
+ } : base.openGraph,
5266
+ twitter: override.twitter ? {
5267
+ ...base.twitter,
5268
+ ...override.twitter
5269
+ } : base.twitter,
5270
+ // Arrays: override replaces base entirely (not merged)
5271
+ metaTags: override.metaTags ?? base.metaTags,
5272
+ links: override.links ?? base.links
5273
+ };
5274
+ }
4116
5275
  function isDataRequest(req) {
4117
5276
  return req.query && req.query.__fw_data === "1" || req.headers["x-fw-data"] === "1";
4118
5277
  }
@@ -4177,22 +5336,59 @@ async function handlePageRequestInternal(options) {
4177
5336
  pathname: urlPath,
4178
5337
  locals: {}
4179
5338
  };
4180
- let loaderResult2 = await runRouteLoader(notFoundPage, ctx2);
5339
+ const layoutProps2 = {};
5340
+ if (notFoundPage.layoutServerHooks && notFoundPage.layoutServerHooks.length > 0) {
5341
+ for (let i = 0; i < notFoundPage.layoutServerHooks.length; i++) {
5342
+ const layoutServerHook = notFoundPage.layoutServerHooks[i];
5343
+ if (layoutServerHook) {
5344
+ try {
5345
+ const layoutResult = await layoutServerHook(ctx2);
5346
+ if (layoutResult.props) {
5347
+ Object.assign(layoutProps2, layoutResult.props);
5348
+ }
5349
+ } catch (error) {
5350
+ const reqLogger2 = getRequestLogger(req);
5351
+ const layoutFile = notFoundPage.layoutFiles[i];
5352
+ const relativeLayoutPath = layoutFile ? import_path23.default.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5353
+ reqLogger2.warn("Layout server hook failed for not-found page", {
5354
+ error: error instanceof Error ? error.message : String(error),
5355
+ stack: error instanceof Error ? error.stack : void 0,
5356
+ layoutFile: relativeLayoutPath,
5357
+ layoutIndex: i
5358
+ });
5359
+ }
5360
+ }
5361
+ }
5362
+ }
5363
+ let loaderResult2 = await runRouteServerHook(notFoundPage, ctx2);
4181
5364
  if (!loaderResult2.theme) {
4182
5365
  loaderResult2.theme = theme;
4183
5366
  }
4184
- const initialData2 = buildInitialData(urlPath, {}, loaderResult2);
5367
+ const combinedProps2 = {
5368
+ ...layoutProps2,
5369
+ ...loaderResult2.props || {}
5370
+ };
5371
+ const combinedLoaderResult2 = {
5372
+ ...loaderResult2,
5373
+ props: combinedProps2
5374
+ };
5375
+ const initialData2 = buildInitialData(urlPath, {}, combinedLoaderResult2);
4185
5376
  const appTree2 = buildAppTree(notFoundPage, {}, initialData2.props);
4186
5377
  initialData2.notFound = true;
4187
5378
  const nonce2 = res.locals.nonce || void 0;
5379
+ const entrypointFiles2 = [];
5380
+ if (assetManifest?.entrypoints?.client) {
5381
+ entrypointFiles2.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5382
+ }
4188
5383
  const documentTree2 = createDocumentTree({
4189
5384
  appTree: appTree2,
4190
5385
  initialData: initialData2,
4191
5386
  routerData,
4192
- meta: loaderResult2.metadata ?? null,
5387
+ meta: combinedLoaderResult2.metadata ?? null,
4193
5388
  titleFallback: "Not found",
4194
5389
  descriptionFallback: "Loly demo",
4195
5390
  chunkHref: null,
5391
+ entrypointFiles: entrypointFiles2,
4196
5392
  theme,
4197
5393
  clientJsPath,
4198
5394
  clientCssPath,
@@ -4208,8 +5404,8 @@ async function handlePageRequestInternal(options) {
4208
5404
  },
4209
5405
  onShellError(err) {
4210
5406
  didError2 = true;
4211
- const reqLogger = getRequestLogger(req);
4212
- reqLogger.error("SSR shell error", err, { route: "not-found" });
5407
+ const reqLogger2 = getRequestLogger(req);
5408
+ reqLogger2.error("SSR shell error", err, { route: "not-found" });
4213
5409
  if (!res.headersSent && errorPage) {
4214
5410
  renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
4215
5411
  } else if (!res.headersSent) {
@@ -4221,8 +5417,8 @@ async function handlePageRequestInternal(options) {
4221
5417
  },
4222
5418
  onError(err) {
4223
5419
  didError2 = true;
4224
- const reqLogger = getRequestLogger(req);
4225
- reqLogger.error("SSR error", err, { route: "not-found" });
5420
+ const reqLogger2 = getRequestLogger(req);
5421
+ reqLogger2.error("SSR error", err, { route: "not-found" });
4226
5422
  }
4227
5423
  });
4228
5424
  req.on("close", () => abort2());
@@ -4244,17 +5440,62 @@ async function handlePageRequestInternal(options) {
4244
5440
  if (res.headersSent) {
4245
5441
  return;
4246
5442
  }
5443
+ const layoutProps = {};
5444
+ const layoutMetadata = [];
5445
+ const reqLogger = getRequestLogger(req);
5446
+ if (route.layoutServerHooks && route.layoutServerHooks.length > 0) {
5447
+ for (let i = 0; i < route.layoutServerHooks.length; i++) {
5448
+ const layoutServerHook = route.layoutServerHooks[i];
5449
+ if (layoutServerHook) {
5450
+ try {
5451
+ const layoutResult = await layoutServerHook(ctx);
5452
+ if (layoutResult.props) {
5453
+ Object.assign(layoutProps, layoutResult.props);
5454
+ }
5455
+ if (layoutResult.metadata) {
5456
+ layoutMetadata.push(layoutResult.metadata);
5457
+ }
5458
+ } catch (error) {
5459
+ const layoutFile = route.layoutFiles[i];
5460
+ const relativeLayoutPath = layoutFile ? import_path23.default.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5461
+ reqLogger.warn("Layout server hook failed", {
5462
+ error: error instanceof Error ? error.message : String(error),
5463
+ stack: error instanceof Error ? error.stack : void 0,
5464
+ layoutFile: relativeLayoutPath,
5465
+ route: route.pattern,
5466
+ layoutIndex: i,
5467
+ suggestion: "Check your layout.server.hook.ts file for errors"
5468
+ });
5469
+ }
5470
+ }
5471
+ }
5472
+ }
4247
5473
  let loaderResult;
4248
5474
  try {
4249
- loaderResult = await runRouteLoader(route, ctx);
5475
+ loaderResult = await runRouteServerHook(route, ctx);
4250
5476
  if (!loaderResult.theme) {
4251
5477
  loaderResult.theme = theme;
4252
5478
  }
4253
5479
  } catch (error) {
5480
+ const relativePagePath = route.pageFile ? import_path23.default.relative(projectRoot || process.cwd(), route.pageFile) : "unknown";
5481
+ reqLogger.error("Page server hook failed", {
5482
+ error: error instanceof Error ? error.message : String(error),
5483
+ stack: error instanceof Error ? error.stack : void 0,
5484
+ pageFile: relativePagePath,
5485
+ route: route.pattern,
5486
+ pathname: urlPath,
5487
+ suggestion: "Check your page.server.hook.ts (or server.hook.ts) file for errors"
5488
+ });
4254
5489
  if (isDataReq) {
4255
5490
  res.statusCode = 500;
4256
5491
  res.setHeader("Content-Type", "application/json; charset=utf-8");
4257
- res.end(JSON.stringify({ error: true, message: String(error) }));
5492
+ const errorResponse = {
5493
+ error: true,
5494
+ message: error instanceof Error ? error.message : String(error),
5495
+ route: route.pattern,
5496
+ pageFile: relativePagePath
5497
+ };
5498
+ res.end(JSON.stringify(errorResponse, null, 2));
4258
5499
  return;
4259
5500
  } else {
4260
5501
  if (errorPage) {
@@ -4265,8 +5506,28 @@ async function handlePageRequestInternal(options) {
4265
5506
  }
4266
5507
  }
4267
5508
  }
5509
+ const combinedProps = {
5510
+ ...layoutProps,
5511
+ // Props from layouts (stable)
5512
+ ...loaderResult.props || {}
5513
+ // Props from page (overrides layout)
5514
+ };
5515
+ let combinedMetadata = null;
5516
+ for (const layoutMeta of layoutMetadata) {
5517
+ if (layoutMeta) {
5518
+ combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
5519
+ }
5520
+ }
5521
+ if (loaderResult.metadata) {
5522
+ combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
5523
+ }
5524
+ const combinedLoaderResult = {
5525
+ ...loaderResult,
5526
+ props: combinedProps,
5527
+ metadata: combinedMetadata
5528
+ };
4268
5529
  if (isDataReq) {
4269
- handleDataResponse(res, loaderResult, theme);
5530
+ handleDataResponse(res, combinedLoaderResult, theme);
4270
5531
  return;
4271
5532
  }
4272
5533
  if (loaderResult.redirect) {
@@ -4281,7 +5542,7 @@ async function handlePageRequestInternal(options) {
4281
5542
  }
4282
5543
  return;
4283
5544
  }
4284
- const initialData = buildInitialData(urlPath, params, loaderResult);
5545
+ const initialData = buildInitialData(urlPath, params, combinedLoaderResult);
4285
5546
  const appTree = buildAppTree(route, params, initialData.props);
4286
5547
  const chunkName = routeChunks[route.pattern];
4287
5548
  let chunkHref = null;
@@ -4293,14 +5554,19 @@ async function handlePageRequestInternal(options) {
4293
5554
  }
4294
5555
  }
4295
5556
  const nonce = res.locals.nonce || void 0;
5557
+ const entrypointFiles = [];
5558
+ if (assetManifest?.entrypoints?.client) {
5559
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5560
+ }
4296
5561
  const documentTree = createDocumentTree({
4297
5562
  appTree,
4298
5563
  initialData,
4299
5564
  routerData,
4300
- meta: loaderResult.metadata,
5565
+ meta: combinedLoaderResult.metadata,
4301
5566
  titleFallback: "Loly framework",
4302
5567
  descriptionFallback: "Loly demo",
4303
5568
  chunkHref,
5569
+ entrypointFiles,
4304
5570
  theme,
4305
5571
  clientJsPath,
4306
5572
  clientCssPath,
@@ -4318,8 +5584,17 @@ async function handlePageRequestInternal(options) {
4318
5584
  },
4319
5585
  onShellError(err) {
4320
5586
  didError = true;
4321
- const reqLogger = getRequestLogger(req);
4322
- reqLogger.error("SSR shell error", err, { route: matched?.route?.pattern || "unknown" });
5587
+ const reqLogger2 = getRequestLogger(req);
5588
+ const routePattern = matched?.route?.pattern || "unknown";
5589
+ reqLogger2.error("SSR shell error", err, { route: routePattern });
5590
+ const errorMessage = err instanceof Error ? err.message : String(err);
5591
+ console.error(`
5592
+ \u274C [framework][ssr] Shell error for route "${routePattern}":`);
5593
+ console.error(` ${errorMessage}`);
5594
+ if (err instanceof Error && err.stack) {
5595
+ console.error(` Stack: ${err.stack.split("\n").slice(0, 3).join("\n ")}`);
5596
+ }
5597
+ console.error("\u{1F4A1} This usually indicates a React rendering error\n");
4323
5598
  if (!res.headersSent && errorPage) {
4324
5599
  renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
4325
5600
  } else if (!res.headersSent) {
@@ -4331,8 +5606,12 @@ async function handlePageRequestInternal(options) {
4331
5606
  },
4332
5607
  onError(err) {
4333
5608
  didError = true;
4334
- const reqLogger = getRequestLogger(req);
4335
- reqLogger.error("SSR error", err, { route: matched?.route?.pattern || "unknown" });
5609
+ const reqLogger2 = getRequestLogger(req);
5610
+ const routePattern = matched?.route?.pattern || "unknown";
5611
+ reqLogger2.error("SSR error", err, { route: routePattern });
5612
+ const errorMessage = err instanceof Error ? err.message : String(err);
5613
+ console.error(`\u26A0\uFE0F [framework][ssr] Error during streaming for route "${routePattern}":`);
5614
+ console.error(` ${errorMessage}`);
4336
5615
  }
4337
5616
  });
4338
5617
  req.on("close", () => {
@@ -4349,11 +5628,43 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4349
5628
  pathname: req.path,
4350
5629
  locals: { error }
4351
5630
  };
4352
- let loaderResult = await runRouteLoader(errorPage, ctx);
5631
+ const layoutProps = {};
5632
+ const reqLogger = getRequestLogger(req);
5633
+ if (errorPage.layoutServerHooks && errorPage.layoutServerHooks.length > 0) {
5634
+ for (let i = 0; i < errorPage.layoutServerHooks.length; i++) {
5635
+ const layoutServerHook = errorPage.layoutServerHooks[i];
5636
+ if (layoutServerHook) {
5637
+ try {
5638
+ const layoutResult = await layoutServerHook(ctx);
5639
+ if (layoutResult.props) {
5640
+ Object.assign(layoutProps, layoutResult.props);
5641
+ }
5642
+ } catch (err) {
5643
+ const layoutFile = errorPage.layoutFiles[i];
5644
+ const relativeLayoutPath = layoutFile ? import_path23.default.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5645
+ reqLogger.warn("Layout server hook failed for error page", {
5646
+ error: err instanceof Error ? err.message : String(err),
5647
+ stack: err instanceof Error ? err.stack : void 0,
5648
+ layoutFile: relativeLayoutPath,
5649
+ layoutIndex: i
5650
+ });
5651
+ }
5652
+ }
5653
+ }
5654
+ }
5655
+ let loaderResult = await runRouteServerHook(errorPage, ctx);
4353
5656
  if (!loaderResult.theme && theme) {
4354
5657
  loaderResult.theme = theme;
4355
5658
  }
4356
- const initialData = buildInitialData(req.path, { error: String(error) }, loaderResult);
5659
+ const combinedProps = {
5660
+ ...layoutProps,
5661
+ ...loaderResult.props || {}
5662
+ };
5663
+ const combinedLoaderResult = {
5664
+ ...loaderResult,
5665
+ props: combinedProps
5666
+ };
5667
+ const initialData = buildInitialData(req.path, { error: String(error) }, combinedLoaderResult);
4357
5668
  const routerData = buildRouterData(req);
4358
5669
  initialData.error = true;
4359
5670
  if (isDataReq) {
@@ -4363,8 +5674,8 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4363
5674
  error: true,
4364
5675
  message: String(error),
4365
5676
  props: initialData.props,
4366
- metadata: loaderResult.metadata ?? null,
4367
- theme: loaderResult.theme ?? theme ?? null
5677
+ metadata: combinedLoaderResult.metadata ?? null,
5678
+ theme: combinedLoaderResult.theme ?? theme ?? null
4368
5679
  }));
4369
5680
  return;
4370
5681
  }
@@ -4382,11 +5693,15 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4382
5693
  }
4383
5694
  }
4384
5695
  const nonce = res.locals.nonce || void 0;
5696
+ const entrypointFiles = [];
5697
+ if (assetManifest?.entrypoints?.client) {
5698
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5699
+ }
4385
5700
  const documentTree = createDocumentTree({
4386
5701
  appTree,
4387
5702
  initialData,
4388
5703
  routerData,
4389
- meta: loaderResult.metadata ?? null,
5704
+ meta: combinedLoaderResult.metadata ?? null,
4390
5705
  titleFallback: "Error",
4391
5706
  descriptionFallback: "An error occurred",
4392
5707
  chunkHref,
@@ -4407,8 +5722,8 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4407
5722
  },
4408
5723
  onShellError(err) {
4409
5724
  didError = true;
4410
- const reqLogger = getRequestLogger(req);
4411
- reqLogger.error("Error rendering error page", err, { type: "shellError" });
5725
+ const reqLogger2 = getRequestLogger(req);
5726
+ reqLogger2.error("Error rendering error page", err, { type: "shellError" });
4412
5727
  if (!res.headersSent) {
4413
5728
  res.statusCode = 500;
4414
5729
  res.setHeader("Content-Type", "text/html; charset=utf-8");
@@ -4418,8 +5733,8 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4418
5733
  },
4419
5734
  onError(err) {
4420
5735
  didError = true;
4421
- const reqLogger = getRequestLogger(req);
4422
- reqLogger.error("Error in error page", err);
5736
+ const reqLogger2 = getRequestLogger(req);
5737
+ reqLogger2.error("Error in error page", err);
4423
5738
  }
4424
5739
  });
4425
5740
  req.on("close", () => {
@@ -4507,7 +5822,7 @@ async function getServerConfig(projectRoot) {
4507
5822
  }
4508
5823
 
4509
5824
  // modules/server/routes.ts
4510
- var import_path21 = __toESM(require("path"));
5825
+ var import_path24 = __toESM(require("path"));
4511
5826
  function setupRoutes(options) {
4512
5827
  const {
4513
5828
  app,
@@ -4522,21 +5837,23 @@ function setupRoutes(options) {
4522
5837
  config
4523
5838
  } = options;
4524
5839
  const routeChunks = routeLoader.loadRouteChunks();
4525
- const ssgOutDir = import_path21.default.join(
4526
- config ? getBuildDir(projectRoot, config) : import_path21.default.join(projectRoot, BUILD_FOLDER_NAME),
5840
+ const ssgOutDir = import_path24.default.join(
5841
+ config ? getBuildDir(projectRoot, config) : import_path24.default.join(projectRoot, BUILD_FOLDER_NAME),
4527
5842
  "ssg"
4528
5843
  );
4529
5844
  app.all("/api/*", async (req, res) => {
4530
5845
  const apiRoutes = isDev && getRoutes ? getRoutes().apiRoutes : initialApiRoutes;
4531
5846
  const serverConfig = await getServerConfig(projectRoot);
4532
5847
  const strictPatterns = serverConfig.rateLimit?.strictPatterns || [];
5848
+ const rateLimitConfig = serverConfig.rateLimit;
4533
5849
  await handleApiRequest({
4534
5850
  apiRoutes,
4535
5851
  urlPath: req.path,
4536
5852
  req,
4537
5853
  res,
4538
5854
  env: isDev ? "dev" : "prod",
4539
- strictRateLimitPatterns: strictPatterns
5855
+ strictRateLimitPatterns: strictPatterns,
5856
+ rateLimitConfig
4540
5857
  });
4541
5858
  });
4542
5859
  app.get("*", async (req, res) => {
@@ -4789,12 +6106,29 @@ var setupApplication = async ({
4789
6106
  corsOptions.origin = process.env.NODE_ENV === "development";
4790
6107
  }
4791
6108
  app.use((0, import_cors.default)(corsOptions));
4792
- if (rateLimit2 && process.env.NODE_ENV !== "development") {
4793
- const generalLimiter = createRateLimiter({
4794
- windowMs: rateLimit2.windowMs,
4795
- max: rateLimit2.max
4796
- });
4797
- app.use(generalLimiter);
6109
+ if (rateLimit2) {
6110
+ const shouldApply = process.env.NODE_ENV !== "development" || process.env.ENABLE_RATE_LIMIT === "true";
6111
+ if (shouldApply) {
6112
+ try {
6113
+ const generalLimiter = createRateLimiterFromConfig(rateLimit2, false);
6114
+ if (generalLimiter) {
6115
+ app.use(generalLimiter);
6116
+ const logger5 = createModuleLogger("server");
6117
+ logger5.info("Rate limiting enabled", {
6118
+ windowMs: rateLimit2.windowMs ?? 15 * 60 * 1e3,
6119
+ max: rateLimit2.max ?? 100,
6120
+ apiMax: rateLimit2.apiMax,
6121
+ strictMax: rateLimit2.strictMax,
6122
+ strictPatterns: rateLimit2.strictPatterns?.length ?? 0
6123
+ });
6124
+ }
6125
+ } catch (error) {
6126
+ const logger5 = createModuleLogger("server");
6127
+ logger5.error("Failed to setup rate limiting", {
6128
+ error: error instanceof Error ? error.message : String(error)
6129
+ });
6130
+ }
6131
+ }
4798
6132
  }
4799
6133
  app.use((0, import_cookie_parser.default)());
4800
6134
  app.use(import_express2.default.json({ limit: bodyLimit }));
@@ -4809,22 +6143,31 @@ var setupApplication = async ({
4809
6143
 
4810
6144
  // src/server.ts
4811
6145
  var import_dotenv2 = __toESM(require("dotenv"));
4812
- var envPath = import_path22.default.join(process.cwd(), ".env");
6146
+ var envPath = import_path25.default.join(process.cwd(), ".env");
4813
6147
  if (import_fs18.default.existsSync(envPath)) {
4814
6148
  import_dotenv2.default.config({ path: envPath });
4815
6149
  } else {
4816
6150
  import_dotenv2.default.config();
4817
6151
  }
4818
- var logger3 = createModuleLogger("server");
6152
+ var logger4 = createModuleLogger("server");
4819
6153
  async function startServer(options = {}) {
4820
6154
  const isDev = options.isDev ?? process.env.NODE_ENV === "development";
4821
6155
  const projectRoot = options.rootDir ?? process.cwd();
4822
- const config = options.config ?? loadConfig(projectRoot);
6156
+ let config;
6157
+ try {
6158
+ config = options.config ?? loadConfig(projectRoot);
6159
+ } catch (error) {
6160
+ if (error instanceof ConfigValidationError) {
6161
+ console.error("\n" + error.message + "\n");
6162
+ process.exit(1);
6163
+ }
6164
+ throw error;
6165
+ }
4823
6166
  const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? config.server.port;
4824
6167
  const host = process.env.HOST ?? (!isDev ? "0.0.0.0" : void 0) ?? config.server.host;
4825
- const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : import_path22.default.join(getBuildDir(projectRoot, config), "server"));
6168
+ const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : import_path25.default.join(getBuildDir(projectRoot, config), "server"));
4826
6169
  if (!isDev && !import_fs18.default.existsSync(appDir)) {
4827
- logger3.error("Compiled directory not found", void 0, {
6170
+ logger4.error("Compiled directory not found", void 0, {
4828
6171
  buildDir: config.directories.build,
4829
6172
  appDir,
4830
6173
  environment: "production"
@@ -4841,7 +6184,7 @@ async function startServer(options = {}) {
4841
6184
  isDev,
4842
6185
  config
4843
6186
  });
4844
- const routeLoader = isDev ? new FilesystemRouteLoader(appDir) : new ManifestRouteLoader(projectRoot);
6187
+ const routeLoader = isDev ? new FilesystemRouteLoader(appDir, projectRoot) : new ManifestRouteLoader(projectRoot);
4845
6188
  setupWssEvents({
4846
6189
  httpServer,
4847
6190
  wssRoutes
@@ -4893,10 +6236,10 @@ async function startProdServer(options = {}) {
4893
6236
  }
4894
6237
 
4895
6238
  // modules/build/ssg/builder.ts
4896
- var import_path26 = __toESM(require("path"));
6239
+ var import_path29 = __toESM(require("path"));
4897
6240
 
4898
6241
  // modules/build/ssg/path.ts
4899
- var import_path23 = __toESM(require("path"));
6242
+ var import_path26 = __toESM(require("path"));
4900
6243
  function buildPathFromPattern(pattern, params) {
4901
6244
  const segments = pattern.split("/").filter(Boolean);
4902
6245
  const parts = [];
@@ -4925,12 +6268,12 @@ function buildPathFromPattern(pattern, params) {
4925
6268
  }
4926
6269
  function pathToOutDir(baseDir, urlPath) {
4927
6270
  const clean = urlPath === "/" ? "" : urlPath.replace(/^\/+/, "");
4928
- return import_path23.default.join(baseDir, clean);
6271
+ return import_path26.default.join(baseDir, clean);
4929
6272
  }
4930
6273
 
4931
6274
  // modules/build/ssg/renderer.ts
4932
6275
  var import_fs19 = __toESM(require("fs"));
4933
- var import_path24 = __toESM(require("path"));
6276
+ var import_path27 = __toESM(require("path"));
4934
6277
  var import_server3 = require("react-dom/server");
4935
6278
  init_globals();
4936
6279
  async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params) {
@@ -4947,6 +6290,10 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
4947
6290
  chunkHref = `${STATIC_PATH}/${chunkName}.js`;
4948
6291
  }
4949
6292
  }
6293
+ const entrypointFiles = [];
6294
+ if (assetManifest?.entrypoints?.client) {
6295
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
6296
+ }
4950
6297
  const req = {
4951
6298
  method: "GET",
4952
6299
  headers: {},
@@ -4976,32 +6323,79 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
4976
6323
  })
4977
6324
  );
4978
6325
  }
6326
+ const layoutProps = {};
6327
+ const layoutMetadata = [];
6328
+ if (route.layoutServerHooks && route.layoutServerHooks.length > 0) {
6329
+ for (let i = 0; i < route.layoutServerHooks.length; i++) {
6330
+ const layoutServerHook = route.layoutServerHooks[i];
6331
+ if (layoutServerHook) {
6332
+ try {
6333
+ const layoutResult = await layoutServerHook(ctx);
6334
+ if (layoutResult.props) {
6335
+ Object.assign(layoutProps, layoutResult.props);
6336
+ }
6337
+ if (layoutResult.metadata) {
6338
+ layoutMetadata.push(layoutResult.metadata);
6339
+ }
6340
+ } catch (error) {
6341
+ console.warn(
6342
+ `\u26A0\uFE0F [framework][ssg] Layout server hook ${i} failed for route ${route.pattern}:`,
6343
+ error instanceof Error ? error.message : String(error)
6344
+ );
6345
+ if (error instanceof Error && error.stack) {
6346
+ console.warn(` Stack: ${error.stack.split("\n").slice(0, 3).join("\n ")}`);
6347
+ }
6348
+ }
6349
+ }
6350
+ }
6351
+ }
4979
6352
  let loaderResult = { props: {} };
4980
6353
  if (route.loader) {
4981
6354
  loaderResult = await route.loader(ctx);
4982
6355
  }
4983
- if (loaderResult.redirect || loaderResult.notFound) {
6356
+ const combinedProps = {
6357
+ ...layoutProps,
6358
+ ...loaderResult.props || {}
6359
+ };
6360
+ let combinedMetadata = null;
6361
+ for (const layoutMeta of layoutMetadata) {
6362
+ if (layoutMeta) {
6363
+ combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
6364
+ }
6365
+ }
6366
+ if (loaderResult.metadata) {
6367
+ combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
6368
+ }
6369
+ const combinedLoaderResult = {
6370
+ ...loaderResult,
6371
+ props: combinedProps,
6372
+ metadata: combinedMetadata
6373
+ };
6374
+ if (combinedLoaderResult.redirect || combinedLoaderResult.notFound) {
4984
6375
  return;
4985
6376
  }
4986
- const initialData = buildInitialData(urlPath, params, loaderResult);
6377
+ const initialData = buildInitialData(urlPath, params, combinedLoaderResult);
4987
6378
  const routerData = buildRouterData(req);
4988
6379
  const appTree = buildAppTree(route, params, initialData.props);
4989
6380
  const documentTree = createDocumentTree({
4990
6381
  appTree,
4991
6382
  initialData,
4992
6383
  routerData,
4993
- meta: loaderResult.metadata,
6384
+ meta: combinedLoaderResult.metadata,
4994
6385
  titleFallback: "My Framework Dev",
4995
6386
  descriptionFallback: "Static page generated by @lolyjs/core.",
4996
6387
  chunkHref,
6388
+ entrypointFiles,
4997
6389
  clientJsPath,
4998
- clientCssPath
6390
+ clientCssPath,
6391
+ includeInlineScripts: true
6392
+ // SSG needs inline scripts (renderToString doesn't support bootstrapScripts)
4999
6393
  });
5000
6394
  const html = "<!DOCTYPE html>" + (0, import_server3.renderToString)(documentTree);
5001
6395
  const dir = pathToOutDir(ssgOutDir, urlPath);
5002
6396
  ensureDir(dir);
5003
- const htmlFile = import_path24.default.join(dir, "index.html");
5004
- const dataFile = import_path24.default.join(dir, "data.json");
6397
+ const htmlFile = import_path27.default.join(dir, "index.html");
6398
+ const dataFile = import_path27.default.join(dir, "data.json");
5005
6399
  import_fs19.default.writeFileSync(htmlFile, html, "utf-8");
5006
6400
  import_fs19.default.writeFileSync(dataFile, JSON.stringify(initialData, null, 2), "utf-8");
5007
6401
  }
@@ -5009,7 +6403,7 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
5009
6403
  // modules/build/ssg/builder.ts
5010
6404
  init_globals();
5011
6405
  async function buildStaticPages(projectRoot, routes) {
5012
- const ssgOutDir = import_path26.default.join(projectRoot, BUILD_FOLDER_NAME, "ssg");
6406
+ const ssgOutDir = import_path29.default.join(projectRoot, BUILD_FOLDER_NAME, "ssg");
5013
6407
  ensureDir(ssgOutDir);
5014
6408
  for (const route of routes) {
5015
6409
  if (route.dynamic !== "force-static") continue;
@@ -5019,12 +6413,15 @@ async function buildStaticPages(projectRoot, routes) {
5019
6413
  } else {
5020
6414
  if (!route.generateStaticParams) {
5021
6415
  console.warn(
5022
- `[framework][ssg] Route ${route.pattern} is marked as force-static but has no generateStaticParams function. Skipping.`
6416
+ `\u26A0\uFE0F [framework][ssg] Route "${route.pattern}" is marked as force-static but has no generateStaticParams function`
5023
6417
  );
6418
+ console.warn(` \u{1F4A1} Add a generateStaticParams export to enable static generation for this route`);
6419
+ console.warn(` Skipping this route...
6420
+ `);
5024
6421
  continue;
5025
6422
  }
5026
6423
  try {
5027
- console.log(`[framework][ssg] Generating static params for route: ${route.pattern}`);
6424
+ console.log(`\u{1F4E6} [framework][ssg] Generating static params for route: ${route.pattern}`);
5028
6425
  let timeoutId = null;
5029
6426
  const timeoutPromise = new Promise((_, reject) => {
5030
6427
  timeoutId = setTimeout(() => {
@@ -5039,12 +6436,16 @@ async function buildStaticPages(projectRoot, routes) {
5039
6436
  clearTimeout(timeoutId);
5040
6437
  }
5041
6438
  allParams = sp;
5042
- console.log(`[framework][ssg] Generated ${sp.length} static params for route: ${route.pattern}`);
6439
+ console.log(` \u2705 Generated ${sp.length} static params for route: ${route.pattern}`);
5043
6440
  } catch (error) {
5044
- console.error(
5045
- `[framework][ssg] Error generating static params for route ${route.pattern}:`,
5046
- error
5047
- );
6441
+ console.error(`
6442
+ \u274C [framework][ssg] Error generating static params for route "${route.pattern}":`);
6443
+ console.error(error instanceof Error ? error.message : String(error));
6444
+ if (error instanceof Error && error.stack) {
6445
+ console.error(` Stack: ${error.stack.split("\n").slice(0, 3).join("\n ")}`);
6446
+ }
6447
+ console.error(`\u{1F4A1} Check your generateStaticParams function for this route
6448
+ `);
5048
6449
  throw error;
5049
6450
  }
5050
6451
  }
@@ -5053,11 +6454,11 @@ async function buildStaticPages(projectRoot, routes) {
5053
6454
  await renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params);
5054
6455
  }
5055
6456
  }
5056
- console.log(`[framework][ssg] Finished building all static pages`);
6457
+ console.log(`\u2705 [framework][ssg] Finished building all static pages`);
5057
6458
  }
5058
6459
 
5059
6460
  // modules/build/bundler/server.ts
5060
- var import_path28 = __toESM(require("path"));
6461
+ var import_path31 = __toESM(require("path"));
5061
6462
  var import_fs20 = __toESM(require("fs"));
5062
6463
  var import_esbuild = __toESM(require("esbuild"));
5063
6464
  init_globals();
@@ -5067,7 +6468,7 @@ function collectAppSources(appDir) {
5067
6468
  function walk(dir) {
5068
6469
  const items = import_fs20.default.readdirSync(dir, { withFileTypes: true });
5069
6470
  for (const item of items) {
5070
- const full = import_path28.default.join(dir, item.name);
6471
+ const full = import_path31.default.join(dir, item.name);
5071
6472
  if (item.isDirectory()) {
5072
6473
  walk(full);
5073
6474
  continue;
@@ -5084,7 +6485,7 @@ function collectAppSources(appDir) {
5084
6485
  return entries;
5085
6486
  }
5086
6487
  async function buildServerApp(projectRoot, appDir) {
5087
- const outDir = import_path28.default.join(projectRoot, BUILD_FOLDER_NAME, "server");
6488
+ const outDir = import_path31.default.join(projectRoot, BUILD_FOLDER_NAME, "server");
5088
6489
  const entryPoints = collectAppSources(appDir);
5089
6490
  ensureDir(outDir);
5090
6491
  if (entryPoints.length === 0) {
@@ -5102,12 +6503,12 @@ async function buildServerApp(projectRoot, appDir) {
5102
6503
  bundle: true,
5103
6504
  splitting: false,
5104
6505
  logLevel: "info",
5105
- tsconfig: import_path28.default.join(projectRoot, "tsconfig.json"),
6506
+ tsconfig: import_path31.default.join(projectRoot, "tsconfig.json"),
5106
6507
  packages: "external"
5107
6508
  });
5108
6509
  for (const fileName of SERVER_FILES) {
5109
- const initTS = import_path28.default.join(projectRoot, `${fileName}.ts`);
5110
- const initJS = import_path28.default.join(outDir, `${fileName}.js`);
6510
+ const initTS = import_path31.default.join(projectRoot, `${fileName}.ts`);
6511
+ const initJS = import_path31.default.join(outDir, `${fileName}.js`);
5111
6512
  if (import_fs20.default.existsSync(initTS)) {
5112
6513
  await import_esbuild.default.build({
5113
6514
  entryPoints: [initTS],
@@ -5119,7 +6520,7 @@ async function buildServerApp(projectRoot, appDir) {
5119
6520
  sourcemap: true,
5120
6521
  bundle: false,
5121
6522
  logLevel: "info",
5122
- tsconfig: import_path28.default.join(projectRoot, "tsconfig.json")
6523
+ tsconfig: import_path31.default.join(projectRoot, "tsconfig.json")
5123
6524
  });
5124
6525
  }
5125
6526
  }
@@ -5262,22 +6663,158 @@ function matchRouteClient(pathWithSearch, routes) {
5262
6663
  }
5263
6664
 
5264
6665
  // modules/runtime/client/metadata.ts
6666
+ function getOrCreateMeta(selector, attributes) {
6667
+ let meta = document.querySelector(selector);
6668
+ if (!meta) {
6669
+ meta = document.createElement("meta");
6670
+ if (attributes.name) meta.name = attributes.name;
6671
+ if (attributes.property) meta.setAttribute("property", attributes.property);
6672
+ if (attributes.httpEquiv) meta.httpEquiv = attributes.httpEquiv;
6673
+ document.head.appendChild(meta);
6674
+ }
6675
+ return meta;
6676
+ }
6677
+ function getOrCreateLink(rel, href) {
6678
+ const selector = `link[rel="${rel}"]`;
6679
+ let link = document.querySelector(selector);
6680
+ if (!link) {
6681
+ link = document.createElement("link");
6682
+ link.rel = rel;
6683
+ link.href = href;
6684
+ document.head.appendChild(link);
6685
+ } else {
6686
+ link.href = href;
6687
+ }
6688
+ return link;
6689
+ }
5265
6690
  function applyMetadata(md) {
5266
6691
  if (!md) return;
5267
6692
  if (md.title) {
5268
6693
  document.title = md.title;
5269
6694
  }
5270
6695
  if (md.description) {
5271
- let meta = document.querySelector(
5272
- 'meta[name="description"]'
5273
- );
5274
- if (!meta) {
5275
- meta = document.createElement("meta");
5276
- meta.name = "description";
5277
- document.head.appendChild(meta);
5278
- }
6696
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
5279
6697
  meta.content = md.description;
5280
6698
  }
6699
+ if (md.robots) {
6700
+ const meta = getOrCreateMeta('meta[name="robots"]', { name: "robots" });
6701
+ meta.content = md.robots;
6702
+ }
6703
+ if (md.themeColor) {
6704
+ const meta = getOrCreateMeta('meta[name="theme-color"]', { name: "theme-color" });
6705
+ meta.content = md.themeColor;
6706
+ }
6707
+ if (md.viewport) {
6708
+ const meta = getOrCreateMeta('meta[name="viewport"]', { name: "viewport" });
6709
+ meta.content = md.viewport;
6710
+ }
6711
+ if (md.canonical) {
6712
+ getOrCreateLink("canonical", md.canonical);
6713
+ }
6714
+ if (md.openGraph) {
6715
+ const og = md.openGraph;
6716
+ if (og.title) {
6717
+ const meta = getOrCreateMeta('meta[property="og:title"]', { property: "og:title" });
6718
+ meta.content = og.title;
6719
+ }
6720
+ if (og.description) {
6721
+ const meta = getOrCreateMeta('meta[property="og:description"]', { property: "og:description" });
6722
+ meta.content = og.description;
6723
+ }
6724
+ if (og.type) {
6725
+ const meta = getOrCreateMeta('meta[property="og:type"]', { property: "og:type" });
6726
+ meta.content = og.type;
6727
+ }
6728
+ if (og.url) {
6729
+ const meta = getOrCreateMeta('meta[property="og:url"]', { property: "og:url" });
6730
+ meta.content = og.url;
6731
+ }
6732
+ if (og.image) {
6733
+ if (typeof og.image === "string") {
6734
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
6735
+ meta.content = og.image;
6736
+ } else {
6737
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
6738
+ meta.content = og.image.url;
6739
+ if (og.image.width) {
6740
+ const metaWidth = getOrCreateMeta('meta[property="og:image:width"]', { property: "og:image:width" });
6741
+ metaWidth.content = String(og.image.width);
6742
+ }
6743
+ if (og.image.height) {
6744
+ const metaHeight = getOrCreateMeta('meta[property="og:image:height"]', { property: "og:image:height" });
6745
+ metaHeight.content = String(og.image.height);
6746
+ }
6747
+ if (og.image.alt) {
6748
+ const metaAlt = getOrCreateMeta('meta[property="og:image:alt"]', { property: "og:image:alt" });
6749
+ metaAlt.content = og.image.alt;
6750
+ }
6751
+ }
6752
+ }
6753
+ if (og.siteName) {
6754
+ const meta = getOrCreateMeta('meta[property="og:site_name"]', { property: "og:site_name" });
6755
+ meta.content = og.siteName;
6756
+ }
6757
+ if (og.locale) {
6758
+ const meta = getOrCreateMeta('meta[property="og:locale"]', { property: "og:locale" });
6759
+ meta.content = og.locale;
6760
+ }
6761
+ }
6762
+ if (md.twitter) {
6763
+ const twitter = md.twitter;
6764
+ if (twitter.card) {
6765
+ const meta = getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" });
6766
+ meta.content = twitter.card;
6767
+ }
6768
+ if (twitter.title) {
6769
+ const meta = getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" });
6770
+ meta.content = twitter.title;
6771
+ }
6772
+ if (twitter.description) {
6773
+ const meta = getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" });
6774
+ meta.content = twitter.description;
6775
+ }
6776
+ if (twitter.image) {
6777
+ const meta = getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" });
6778
+ meta.content = twitter.image;
6779
+ }
6780
+ if (twitter.imageAlt) {
6781
+ const meta = getOrCreateMeta('meta[name="twitter:image:alt"]', { name: "twitter:image:alt" });
6782
+ meta.content = twitter.imageAlt;
6783
+ }
6784
+ if (twitter.site) {
6785
+ const meta = getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" });
6786
+ meta.content = twitter.site;
6787
+ }
6788
+ if (twitter.creator) {
6789
+ const meta = getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" });
6790
+ meta.content = twitter.creator;
6791
+ }
6792
+ }
6793
+ if (md.metaTags && Array.isArray(md.metaTags)) {
6794
+ md.metaTags.forEach((tag) => {
6795
+ let selector = "";
6796
+ if (tag.name) {
6797
+ selector = `meta[name="${tag.name}"]`;
6798
+ } else if (tag.property) {
6799
+ selector = `meta[property="${tag.property}"]`;
6800
+ } else if (tag.httpEquiv) {
6801
+ selector = `meta[http-equiv="${tag.httpEquiv}"]`;
6802
+ }
6803
+ if (selector) {
6804
+ const meta = getOrCreateMeta(selector, {
6805
+ name: tag.name,
6806
+ property: tag.property,
6807
+ httpEquiv: tag.httpEquiv
6808
+ });
6809
+ meta.content = tag.content;
6810
+ }
6811
+ });
6812
+ }
6813
+ if (md.links && Array.isArray(md.links)) {
6814
+ md.links.forEach((link) => {
6815
+ getOrCreateLink(link.rel, link.href);
6816
+ });
6817
+ }
5281
6818
  }
5282
6819
 
5283
6820
  // modules/runtime/client/AppShell.tsx
@@ -5494,10 +7031,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
5494
7031
  });
5495
7032
  return true;
5496
7033
  } catch (loadError) {
5497
- console.error(
5498
- "[client] Error loading error route components:",
5499
- loadError
5500
- );
7034
+ console.error("\n\u274C [client] Error loading error route components:");
7035
+ console.error(loadError);
7036
+ if (loadError instanceof Error) {
7037
+ console.error(` Message: ${loadError.message}`);
7038
+ if (loadError.stack) {
7039
+ console.error(` Stack: ${loadError.stack.split("\n").slice(0, 3).join("\n ")}`);
7040
+ }
7041
+ }
7042
+ console.error("\u{1F4A1} Falling back to full page reload\n");
5501
7043
  window.location.href = nextUrl;
5502
7044
  return false;
5503
7045
  }
@@ -5602,7 +7144,11 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
5602
7144
  searchParams: Object.fromEntries(url.searchParams.entries())
5603
7145
  };
5604
7146
  setRouterData(routerData);
5605
- const components = await matched.route.load();
7147
+ const prefetched = prefetchedRoutes.get(matched.route);
7148
+ const components = prefetched ? await prefetched : await matched.route.load();
7149
+ if (!prefetched) {
7150
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
7151
+ }
5606
7152
  window.scrollTo({
5607
7153
  top: 0,
5608
7154
  behavior: "smooth"
@@ -5641,7 +7187,7 @@ async function navigate(nextUrl, handlers, options) {
5641
7187
  }
5642
7188
  }
5643
7189
  if (!ok) {
5644
- if (json && json.redirect) {
7190
+ if (json?.redirect) {
5645
7191
  window.location.href = json.redirect.destination;
5646
7192
  return;
5647
7193
  }
@@ -5662,6 +7208,47 @@ async function navigate(nextUrl, handlers, options) {
5662
7208
  window.location.href = nextUrl;
5663
7209
  }
5664
7210
  }
7211
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
7212
+ function prefetchRoute(url, routes, notFoundRoute) {
7213
+ const [pathname] = url.split("?");
7214
+ const matched = matchRouteClient(pathname, routes);
7215
+ if (!matched) {
7216
+ if (notFoundRoute) {
7217
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
7218
+ if (!existing2) {
7219
+ const promise = notFoundRoute.load();
7220
+ prefetchedRoutes.set(notFoundRoute, promise);
7221
+ }
7222
+ }
7223
+ return;
7224
+ }
7225
+ const existing = prefetchedRoutes.get(matched.route);
7226
+ if (!existing) {
7227
+ const promise = matched.route.load();
7228
+ prefetchedRoutes.set(matched.route, promise);
7229
+ }
7230
+ }
7231
+ function createHoverHandler(routes, notFoundRoute) {
7232
+ return function handleHover(ev) {
7233
+ try {
7234
+ const target = ev.target;
7235
+ if (!target) return;
7236
+ const anchor = target.closest("a[href]");
7237
+ if (!anchor) return;
7238
+ const href = anchor.getAttribute("href");
7239
+ if (!href) return;
7240
+ if (href.startsWith("#")) return;
7241
+ const url = new URL(href, window.location.href);
7242
+ if (url.origin !== window.location.origin) return;
7243
+ if (anchor.target && anchor.target !== "_self") return;
7244
+ const nextUrl = url.pathname + url.search;
7245
+ const currentUrl = window.location.pathname + window.location.search;
7246
+ if (nextUrl === currentUrl) return;
7247
+ prefetchRoute(nextUrl, routes, notFoundRoute);
7248
+ } catch (error) {
7249
+ }
7250
+ };
7251
+ }
5665
7252
  function createClickHandler(navigate2) {
5666
7253
  return function handleClick(ev) {
5667
7254
  try {
@@ -5772,17 +7359,20 @@ function AppShell({
5772
7359
  }
5773
7360
  const handleClick = createClickHandler(handleNavigateInternal);
5774
7361
  const handlePopState = createPopStateHandler(handleNavigateInternal);
7362
+ const handleHover = createHoverHandler(routes, notFoundRoute);
5775
7363
  window.addEventListener("click", handleClick, false);
5776
7364
  window.addEventListener("popstate", handlePopState, false);
7365
+ window.addEventListener("mouseover", handleHover, false);
5777
7366
  return () => {
5778
7367
  isMounted = false;
5779
7368
  window.removeEventListener("click", handleClick, false);
5780
7369
  window.removeEventListener("popstate", handlePopState, false);
7370
+ window.removeEventListener("mouseover", handleHover, false);
5781
7371
  };
5782
- }, []);
7372
+ }, [routes, notFoundRoute]);
5783
7373
  (0, import_react3.useEffect)(() => {
5784
7374
  const handleDataRefresh = () => {
5785
- const freshData = window?.__FW_DATA__;
7375
+ const freshData = window[WINDOW_DATA_KEY2];
5786
7376
  if (!freshData) return;
5787
7377
  const currentPathname = window.location.pathname;
5788
7378
  const freshPathname = freshData.pathname;
@@ -5809,6 +7399,91 @@ function AppShell({
5809
7399
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterView, { state }, routeKey) });
5810
7400
  }
5811
7401
 
7402
+ // modules/runtime/client/hot-reload.ts
7403
+ function setupHotReload2() {
7404
+ const nodeEnv = process.env.NODE_ENV || "production";
7405
+ const isDev = nodeEnv === "development";
7406
+ console.log(`[hot-reload] NODE_ENV: ${nodeEnv}, isDev: ${isDev}`);
7407
+ if (!isDev) {
7408
+ console.log("[hot-reload] Skipping hot reload setup (not in development mode)");
7409
+ return;
7410
+ }
7411
+ console.log("[hot-reload] Setting up hot reload client...");
7412
+ let eventSource = null;
7413
+ let reloadTimeout = null;
7414
+ let reconnectTimeout = null;
7415
+ let reconnectAttempts = 0;
7416
+ const MAX_RECONNECT_ATTEMPTS = 10;
7417
+ const RECONNECT_DELAY = 1e3;
7418
+ const RELOAD_DELAY = 100;
7419
+ function connect() {
7420
+ try {
7421
+ if (eventSource) {
7422
+ console.log("[hot-reload] Closing existing EventSource connection");
7423
+ eventSource.close();
7424
+ }
7425
+ const endpoint = "/__fw/hot";
7426
+ eventSource = new EventSource(endpoint);
7427
+ eventSource.addEventListener("ping", (event) => {
7428
+ if ("data" in event) {
7429
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
7430
+ }
7431
+ reconnectAttempts = 0;
7432
+ });
7433
+ eventSource.addEventListener("message", (event) => {
7434
+ const data = event.data;
7435
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
7436
+ const filePath = data.slice(7);
7437
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
7438
+ if (reloadTimeout) {
7439
+ clearTimeout(reloadTimeout);
7440
+ }
7441
+ reloadTimeout = setTimeout(() => {
7442
+ try {
7443
+ window.location.reload();
7444
+ } catch (error) {
7445
+ console.error("[hot-reload] \u274C Error reloading page:", error);
7446
+ setTimeout(() => window.location.reload(), 100);
7447
+ }
7448
+ }, RELOAD_DELAY);
7449
+ }
7450
+ });
7451
+ eventSource.onopen = () => {
7452
+ reconnectAttempts = 0;
7453
+ };
7454
+ eventSource.onerror = (error) => {
7455
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
7456
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
7457
+ if (eventSource?.readyState === EventSource.CONNECTING) {
7458
+ console.log("[hot-reload] \u23F3 Still connecting...");
7459
+ return;
7460
+ } else if (eventSource?.readyState === EventSource.OPEN) {
7461
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
7462
+ } else {
7463
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
7464
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
7465
+ reconnectAttempts++;
7466
+ const delay = RECONNECT_DELAY * reconnectAttempts;
7467
+ if (reconnectTimeout) {
7468
+ clearTimeout(reconnectTimeout);
7469
+ }
7470
+ reconnectTimeout = setTimeout(() => {
7471
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
7472
+ connect();
7473
+ }, delay);
7474
+ } else {
7475
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
7476
+ }
7477
+ }
7478
+ };
7479
+ } catch (error) {
7480
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
7481
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
7482
+ }
7483
+ }
7484
+ connect();
7485
+ }
7486
+
5812
7487
  // modules/runtime/client/bootstrap.tsx
5813
7488
  var import_jsx_runtime3 = require("react/jsx-runtime");
5814
7489
  async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute, errorRoute) {
@@ -5850,101 +7525,91 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
5850
7525
  props: initialData?.props ?? {}
5851
7526
  };
5852
7527
  }
5853
- function setupHotReload2() {
5854
- const nodeEnv = typeof process !== "undefined" && process.env?.NODE_ENV || "production";
5855
- const isDev = nodeEnv !== "production";
5856
- if (!isDev) {
5857
- return;
7528
+ function initializeRouterData(initialUrl, initialData) {
7529
+ let routerData = getRouterData();
7530
+ if (!routerData) {
7531
+ const url = new URL(initialUrl, window.location.origin);
7532
+ routerData = {
7533
+ pathname: url.pathname,
7534
+ params: initialData?.params || {},
7535
+ searchParams: Object.fromEntries(url.searchParams.entries())
7536
+ };
7537
+ setRouterData(routerData);
5858
7538
  }
7539
+ }
7540
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
5859
7541
  try {
5860
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
5861
- const eventSource = new EventSource("/__fw/hot");
5862
- let reloadTimeout = null;
5863
- eventSource.addEventListener("message", (event) => {
5864
- const data = event.data;
5865
- if (data && data.startsWith("reload:")) {
5866
- const filePath = data.slice(7);
5867
- console.log(`[hot-reload] File changed: ${filePath}`);
5868
- if (reloadTimeout) {
5869
- clearTimeout(reloadTimeout);
5870
- }
5871
- reloadTimeout = setTimeout(() => {
5872
- console.log("[hot-reload] Reloading page...");
5873
- window.location.reload();
5874
- }, 500);
5875
- }
5876
- });
5877
- eventSource.addEventListener("ping", () => {
5878
- console.log("[hot-reload] \u2713 Connected to hot reload server");
5879
- });
5880
- eventSource.onopen = () => {
5881
- console.log("[hot-reload] \u2713 SSE connection opened");
5882
- };
5883
- eventSource.onerror = (error) => {
5884
- const states = ["CONNECTING", "OPEN", "CLOSED"];
5885
- const state = states[eventSource.readyState] || "UNKNOWN";
5886
- if (eventSource.readyState === EventSource.CONNECTING) {
5887
- console.log("[hot-reload] Connecting...");
5888
- } else if (eventSource.readyState === EventSource.OPEN) {
5889
- console.warn("[hot-reload] Connection error (but connection is open):", error);
5890
- } else {
5891
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
7542
+ const initialState = await loadInitialRoute(
7543
+ initialUrl,
7544
+ initialData,
7545
+ routes,
7546
+ notFoundRoute,
7547
+ errorRoute
7548
+ );
7549
+ if (initialData?.metadata) {
7550
+ try {
7551
+ applyMetadata(initialData.metadata);
7552
+ } catch (metadataError) {
7553
+ console.warn("[client] Error applying metadata:", metadataError);
5892
7554
  }
5893
- };
7555
+ }
7556
+ (0, import_client5.hydrateRoot)(
7557
+ container,
7558
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7559
+ AppShell,
7560
+ {
7561
+ initialState,
7562
+ routes,
7563
+ notFoundRoute,
7564
+ errorRoute
7565
+ }
7566
+ )
7567
+ );
5894
7568
  } catch (error) {
5895
- console.log("[hot-reload] EventSource not supported or error:", error);
7569
+ console.error(
7570
+ "[client] Error loading initial route components for",
7571
+ initialUrl,
7572
+ error
7573
+ );
7574
+ throw error;
5896
7575
  }
5897
7576
  }
5898
7577
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
5899
- console.log("[client] Bootstrap starting, setting up hot reload...");
5900
7578
  setupHotReload2();
5901
- (async function bootstrap() {
5902
- const container = document.getElementById(APP_CONTAINER_ID2);
5903
- const initialData = getWindowData();
5904
- if (!container) {
5905
- console.error(`Container #${APP_CONTAINER_ID2} not found for hydration`);
5906
- return;
5907
- }
5908
- const initialUrl = window.location.pathname + window.location.search;
5909
- let routerData = getRouterData();
5910
- if (!routerData) {
5911
- const url = new URL(initialUrl, window.location.origin);
5912
- routerData = {
5913
- pathname: url.pathname,
5914
- params: initialData?.params || {},
5915
- searchParams: Object.fromEntries(url.searchParams.entries())
5916
- };
5917
- setRouterData(routerData);
5918
- }
7579
+ (async () => {
5919
7580
  try {
5920
- const initialState = await loadInitialRoute(
7581
+ const container = document.getElementById(APP_CONTAINER_ID2);
7582
+ if (!container) {
7583
+ console.error(`
7584
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID2} not found`);
7585
+ console.error("\u{1F4A1} This usually means:");
7586
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
7587
+ console.error(" \u2022 The container was removed before hydration");
7588
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
7589
+ return;
7590
+ }
7591
+ const initialData = getWindowData();
7592
+ const initialUrl = window.location.pathname + window.location.search;
7593
+ initializeRouterData(initialUrl, initialData);
7594
+ await hydrateInitialRoute(
7595
+ container,
5921
7596
  initialUrl,
5922
7597
  initialData,
5923
7598
  routes,
5924
7599
  notFoundRoute,
5925
7600
  errorRoute
5926
7601
  );
5927
- if (initialData?.metadata) {
5928
- applyMetadata(initialData.metadata);
5929
- }
5930
- (0, import_client5.hydrateRoot)(
5931
- container,
5932
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
5933
- AppShell,
5934
- {
5935
- initialState,
5936
- routes,
5937
- notFoundRoute,
5938
- errorRoute
5939
- }
5940
- )
5941
- );
5942
7602
  } catch (error) {
5943
- console.error(
5944
- "[client] Error loading initial route components for",
5945
- initialUrl,
5946
- error
5947
- );
7603
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
7604
+ console.error(error);
7605
+ if (error instanceof Error) {
7606
+ console.error("\nError details:");
7607
+ console.error(` Message: ${error.message}`);
7608
+ if (error.stack) {
7609
+ console.error(` Stack: ${error.stack}`);
7610
+ }
7611
+ }
7612
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
5948
7613
  window.location.reload();
5949
7614
  }
5950
7615
  })();
@@ -5972,11 +7637,11 @@ var ValidationError = class extends Error {
5972
7637
  format() {
5973
7638
  const formatted = {};
5974
7639
  for (const error of this.errors) {
5975
- const path25 = error.path.join(".");
5976
- if (!formatted[path25]) {
5977
- formatted[path25] = [];
7640
+ const path28 = error.path.join(".");
7641
+ if (!formatted[path28]) {
7642
+ formatted[path28] = [];
5978
7643
  }
5979
- formatted[path25].push(error.message);
7644
+ formatted[path28].push(error.message);
5980
7645
  }
5981
7646
  return formatted;
5982
7647
  }