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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -74,7 +74,7 @@ var init_globals = __esm({
74
74
 
75
75
  // src/server.ts
76
76
  import fs16 from "fs";
77
- import path20 from "path";
77
+ import path23 from "path";
78
78
 
79
79
  // modules/server/utils/server-dir.ts
80
80
  init_globals();
@@ -251,9 +251,26 @@ function loadServerHookForDir(currentDir) {
251
251
  generateStaticParams: null
252
252
  };
253
253
  }
254
- const middlewares = Array.isArray(
255
- mod?.[NAMING.BEFORE_MIDDLEWARES]
256
- ) ? mod[NAMING.BEFORE_MIDDLEWARES] : [];
254
+ let middlewares = [];
255
+ const rawMiddlewares = mod?.[NAMING.BEFORE_MIDDLEWARES];
256
+ if (rawMiddlewares !== void 0) {
257
+ if (!Array.isArray(rawMiddlewares)) {
258
+ console.warn(
259
+ `[framework][server-hook] ${NAMING.BEFORE_MIDDLEWARES} must be an array in ${file}, ignoring invalid value`
260
+ );
261
+ } else {
262
+ for (let i = 0; i < rawMiddlewares.length; i++) {
263
+ const mw = rawMiddlewares[i];
264
+ if (typeof mw !== "function") {
265
+ console.warn(
266
+ `[framework][server-hook] Middleware at index ${i} in ${NAMING.BEFORE_MIDDLEWARES} is not a function in ${file}, skipping`
267
+ );
268
+ continue;
269
+ }
270
+ middlewares.push(mw);
271
+ }
272
+ }
273
+ }
257
274
  const serverHook = typeof mod?.[NAMING.GET_SERVER_DATA_FN] === "function" ? mod[NAMING.GET_SERVER_DATA_FN] : null;
258
275
  const dynamic = mod?.[NAMING.RENDER_TYPE_CONST] === "force-static" || mod?.[NAMING.RENDER_TYPE_CONST] === "force-dynamic" ? mod.dynamic : "auto";
259
276
  const generateStaticParams = typeof mod?.[NAMING.GENERATE_SSG_PARAMS] === "function" ? mod[NAMING.GENERATE_SSG_PARAMS] : null;
@@ -293,6 +310,50 @@ function loadLayoutServerHook(layoutFile) {
293
310
  }
294
311
 
295
312
  // modules/router/loader-pages.ts
313
+ function validateRoutes(routes, appDir) {
314
+ const routePatterns = /* @__PURE__ */ new Map();
315
+ const errors = [];
316
+ const warnings = [];
317
+ for (const route of routes) {
318
+ const existing = routePatterns.get(route.pattern) || [];
319
+ existing.push(route);
320
+ routePatterns.set(route.pattern, existing);
321
+ }
322
+ for (const [pattern, duplicateRoutes] of routePatterns.entries()) {
323
+ if (duplicateRoutes.length > 1) {
324
+ const files = duplicateRoutes.map(
325
+ (r) => r.pageFile ? path4.relative(appDir, r.pageFile) : "unknown"
326
+ ).join(", ");
327
+ errors.push(
328
+ `Duplicate route pattern "${pattern}" found in multiple files:
329
+ ${files}
330
+ \u{1F4A1} Suggestion: Ensure each route has a unique path pattern`
331
+ );
332
+ }
333
+ }
334
+ for (const route of routes) {
335
+ if (!route.pageFile || !fs4.existsSync(route.pageFile)) {
336
+ warnings.push(
337
+ `Route pattern "${route.pattern}" references a missing page file`
338
+ );
339
+ }
340
+ }
341
+ if (errors.length > 0) {
342
+ const errorMessage = [
343
+ "\u274C Route validation failed:",
344
+ "",
345
+ ...errors,
346
+ "",
347
+ "\u{1F4A1} Please fix the errors above before starting the server."
348
+ ].join("\n");
349
+ throw new Error(errorMessage);
350
+ }
351
+ if (warnings.length > 0 && process.env.NODE_ENV === "development") {
352
+ console.warn("\n\u26A0\uFE0F Route warnings:");
353
+ warnings.forEach((warning) => console.warn(` \u2022 ${warning}`));
354
+ console.warn("");
355
+ }
356
+ }
296
357
  function loadRoutes(appDir) {
297
358
  if (!fs4.existsSync(appDir)) {
298
359
  return [];
@@ -347,6 +408,7 @@ function loadRoutes(appDir) {
347
408
  }
348
409
  }
349
410
  walk(appDir);
411
+ validateRoutes(routes, appDir);
350
412
  return routes;
351
413
  }
352
414
 
@@ -710,9 +772,12 @@ function writeClientBoostrapManifest(projectRoot) {
710
772
  lines.push("");
711
773
  lines.push(`import { bootstrapClient } from "@lolyjs/core/runtime"`);
712
774
  lines.push("");
713
- lines.push(
714
- "bootstrapClient(routes as ClientRouteLoaded[], notFoundRoute, errorRoute);"
715
- );
775
+ lines.push(`try {`);
776
+ lines.push(` bootstrapClient(routes as ClientRouteLoaded[], notFoundRoute, errorRoute);`);
777
+ lines.push(`} catch (error) {`);
778
+ lines.push(` console.error("[bootstrap] Fatal error during bootstrap:", error);`);
779
+ lines.push(` throw error;`);
780
+ lines.push(`}`);
716
781
  fs7.writeFileSync(manifestPath, lines.join("\n"), "utf-8");
717
782
  }
718
783
  function writeRoutesManifest({
@@ -1050,24 +1115,210 @@ function loadWssRoutes(appDir) {
1050
1115
  }
1051
1116
 
1052
1117
  // modules/router/route-loader.ts
1118
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1119
+ "node_modules",
1120
+ ".git",
1121
+ ".loly",
1122
+ "dist",
1123
+ "build",
1124
+ ".next",
1125
+ ".cache",
1126
+ "coverage",
1127
+ ".vscode",
1128
+ ".idea",
1129
+ ".turbo",
1130
+ ".swc"
1131
+ ]);
1132
+ function getRelevantFiles(appDir, projectRoot) {
1133
+ const files = [];
1134
+ const projectRootNormalized = path10.resolve(projectRoot);
1135
+ function walk(currentDir) {
1136
+ if (!fs10.existsSync(currentDir)) {
1137
+ return;
1138
+ }
1139
+ const entries = fs10.readdirSync(currentDir, { withFileTypes: true });
1140
+ for (const entry of entries) {
1141
+ const fullPath = path10.join(currentDir, entry.name);
1142
+ if (entry.isDirectory()) {
1143
+ if (SKIP_DIRECTORIES.has(entry.name)) {
1144
+ continue;
1145
+ }
1146
+ if (entry.name.startsWith(".")) {
1147
+ continue;
1148
+ }
1149
+ walk(fullPath);
1150
+ } else {
1151
+ const ext = path10.extname(entry.name);
1152
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
1153
+ files.push(fullPath);
1154
+ }
1155
+ }
1156
+ }
1157
+ }
1158
+ walk(projectRootNormalized);
1159
+ return files;
1160
+ }
1161
+ function hasFilesChanged(appDir, projectRoot, cachedStats) {
1162
+ const currentFiles = getRelevantFiles(appDir, projectRoot);
1163
+ const currentFilesSet = new Set(currentFiles);
1164
+ for (const [filePath] of cachedStats.entries()) {
1165
+ if (!currentFilesSet.has(filePath)) {
1166
+ return true;
1167
+ }
1168
+ }
1169
+ for (const filePath of currentFiles) {
1170
+ if (!fs10.existsSync(filePath)) {
1171
+ continue;
1172
+ }
1173
+ const stats = fs10.statSync(filePath);
1174
+ const cachedStat = cachedStats.get(filePath);
1175
+ if (!cachedStat) {
1176
+ return true;
1177
+ }
1178
+ if (stats.mtimeMs !== cachedStat.mtime || stats.size !== cachedStat.size) {
1179
+ return true;
1180
+ }
1181
+ }
1182
+ return false;
1183
+ }
1184
+ function buildFileStats(files) {
1185
+ const statsMap = /* @__PURE__ */ new Map();
1186
+ for (const filePath of files) {
1187
+ if (fs10.existsSync(filePath)) {
1188
+ const stats = fs10.statSync(filePath);
1189
+ statsMap.set(filePath, {
1190
+ mtime: stats.mtimeMs,
1191
+ size: stats.size
1192
+ });
1193
+ }
1194
+ }
1195
+ return statsMap;
1196
+ }
1053
1197
  var FilesystemRouteLoader = class {
1054
- constructor(appDir) {
1198
+ // Maximum cache age in ms (1 second fallback)
1199
+ constructor(appDir, projectRoot = appDir) {
1055
1200
  this.appDir = appDir;
1201
+ this.projectRoot = projectRoot;
1202
+ this.cache = null;
1203
+ this.cacheMaxAge = 1e3;
1204
+ if (this.projectRoot === this.appDir) {
1205
+ let current = path10.resolve(this.appDir);
1206
+ while (current !== path10.dirname(current)) {
1207
+ if (fs10.existsSync(path10.join(current, "package.json"))) {
1208
+ this.projectRoot = current;
1209
+ break;
1210
+ }
1211
+ current = path10.dirname(current);
1212
+ }
1213
+ }
1214
+ }
1215
+ /**
1216
+ * Invalidates the cache, forcing a reload on next access.
1217
+ */
1218
+ invalidateCache() {
1219
+ this.cache = null;
1220
+ }
1221
+ /**
1222
+ * Checks if cache is still valid, invalidates if files changed.
1223
+ */
1224
+ ensureCacheValid() {
1225
+ if (!this.cache) {
1226
+ return;
1227
+ }
1228
+ const now = Date.now();
1229
+ if (now - this.cache.timestamp > this.cacheMaxAge) {
1230
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats)) {
1231
+ this.cache = null;
1232
+ } else {
1233
+ this.cache.timestamp = now;
1234
+ }
1235
+ }
1056
1236
  }
1057
1237
  loadRoutes() {
1058
- return loadRoutes(this.appDir);
1238
+ this.ensureCacheValid();
1239
+ if (!this.cache || hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats)) {
1240
+ const routes = loadRoutes(this.appDir);
1241
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1242
+ const fileStats = buildFileStats(files);
1243
+ this.cache = {
1244
+ routes,
1245
+ apiRoutes: this.cache?.apiRoutes || [],
1246
+ wssRoutes: this.cache?.wssRoutes || [],
1247
+ notFoundRoute: this.cache?.notFoundRoute ?? null,
1248
+ errorRoute: this.cache?.errorRoute ?? null,
1249
+ fileStats,
1250
+ timestamp: Date.now()
1251
+ };
1252
+ }
1253
+ return this.cache.routes;
1059
1254
  }
1060
1255
  loadApiRoutes() {
1061
- return loadApiRoutes(this.appDir);
1256
+ this.ensureCacheValid();
1257
+ if (!this.cache) {
1258
+ this.loadRoutes();
1259
+ }
1260
+ if (!this.cache) {
1261
+ throw new Error("Failed to initialize route cache");
1262
+ }
1263
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.apiRoutes.length === 0) {
1264
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1265
+ const fileStats = buildFileStats(files);
1266
+ this.cache.apiRoutes = loadApiRoutes(this.appDir);
1267
+ this.cache.fileStats = fileStats;
1268
+ this.cache.timestamp = Date.now();
1269
+ }
1270
+ return this.cache.apiRoutes;
1062
1271
  }
1063
1272
  loadWssRoutes() {
1064
- return loadWssRoutes(this.appDir);
1273
+ this.ensureCacheValid();
1274
+ if (!this.cache) {
1275
+ this.loadRoutes();
1276
+ }
1277
+ if (!this.cache) {
1278
+ throw new Error("Failed to initialize route cache");
1279
+ }
1280
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.wssRoutes.length === 0) {
1281
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1282
+ const fileStats = buildFileStats(files);
1283
+ this.cache.wssRoutes = loadWssRoutes(this.appDir);
1284
+ this.cache.fileStats = fileStats;
1285
+ this.cache.timestamp = Date.now();
1286
+ }
1287
+ return this.cache.wssRoutes;
1065
1288
  }
1066
1289
  loadNotFoundRoute() {
1067
- return loadNotFoundRouteFromFilesystem(this.appDir);
1290
+ this.ensureCacheValid();
1291
+ if (!this.cache) {
1292
+ this.loadRoutes();
1293
+ }
1294
+ if (!this.cache) {
1295
+ throw new Error("Failed to initialize route cache");
1296
+ }
1297
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.notFoundRoute === void 0) {
1298
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1299
+ const fileStats = buildFileStats(files);
1300
+ this.cache.notFoundRoute = loadNotFoundRouteFromFilesystem(this.appDir);
1301
+ this.cache.fileStats = fileStats;
1302
+ this.cache.timestamp = Date.now();
1303
+ }
1304
+ return this.cache.notFoundRoute;
1068
1305
  }
1069
1306
  loadErrorRoute() {
1070
- return loadErrorRouteFromFilesystem(this.appDir);
1307
+ this.ensureCacheValid();
1308
+ if (!this.cache) {
1309
+ this.loadRoutes();
1310
+ }
1311
+ if (!this.cache) {
1312
+ throw new Error("Failed to initialize route cache");
1313
+ }
1314
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.errorRoute === void 0) {
1315
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1316
+ const fileStats = buildFileStats(files);
1317
+ this.cache.errorRoute = loadErrorRouteFromFilesystem(this.appDir);
1318
+ this.cache.fileStats = fileStats;
1319
+ this.cache.timestamp = Date.now();
1320
+ }
1321
+ return this.cache.errorRoute;
1071
1322
  }
1072
1323
  loadRouteChunks() {
1073
1324
  return {};
@@ -1076,27 +1327,67 @@ var FilesystemRouteLoader = class {
1076
1327
  var ManifestRouteLoader = class {
1077
1328
  constructor(projectRoot) {
1078
1329
  this.projectRoot = projectRoot;
1330
+ this.cache = {};
1331
+ }
1332
+ /**
1333
+ * Gets the manifest, using cache if available.
1334
+ * The manifest is read once and cached for the lifetime of the loader.
1335
+ */
1336
+ getManifest() {
1337
+ if (this.cache.manifest !== void 0) {
1338
+ return this.cache.manifest;
1339
+ }
1340
+ const manifest = readManifest(this.projectRoot);
1341
+ this.cache.manifest = manifest;
1342
+ return manifest;
1079
1343
  }
1080
1344
  loadRoutes() {
1345
+ if (this.cache.routes) {
1346
+ return this.cache.routes;
1347
+ }
1081
1348
  const { routes } = loadRoutesFromManifest(this.projectRoot);
1349
+ this.cache.routes = routes;
1082
1350
  return routes;
1083
1351
  }
1084
1352
  loadApiRoutes() {
1353
+ if (this.cache.apiRoutes) {
1354
+ return this.cache.apiRoutes;
1355
+ }
1085
1356
  const { apiRoutes } = loadRoutesFromManifest(this.projectRoot);
1357
+ this.cache.apiRoutes = apiRoutes;
1086
1358
  return apiRoutes;
1087
1359
  }
1088
1360
  loadWssRoutes() {
1361
+ if (this.cache.wssRoutes) {
1362
+ return this.cache.wssRoutes;
1363
+ }
1089
1364
  const { wssRoutes } = loadRoutesFromManifest(this.projectRoot);
1365
+ this.cache.wssRoutes = wssRoutes;
1090
1366
  return wssRoutes;
1091
1367
  }
1092
1368
  loadNotFoundRoute() {
1093
- return loadNotFoundFromManifest(this.projectRoot);
1369
+ if (this.cache.notFoundRoute !== void 0) {
1370
+ return this.cache.notFoundRoute;
1371
+ }
1372
+ const route = loadNotFoundFromManifest(this.projectRoot);
1373
+ this.cache.notFoundRoute = route;
1374
+ return route;
1094
1375
  }
1095
1376
  loadErrorRoute() {
1096
- return loadErrorFromManifest(this.projectRoot);
1377
+ if (this.cache.errorRoute !== void 0) {
1378
+ return this.cache.errorRoute;
1379
+ }
1380
+ const route = loadErrorFromManifest(this.projectRoot);
1381
+ this.cache.errorRoute = route;
1382
+ return route;
1097
1383
  }
1098
1384
  loadRouteChunks() {
1099
- return loadChunksFromManifest(this.projectRoot);
1385
+ if (this.cache.routeChunks) {
1386
+ return this.cache.routeChunks;
1387
+ }
1388
+ const chunks = loadChunksFromManifest(this.projectRoot);
1389
+ this.cache.routeChunks = chunks;
1390
+ return chunks;
1100
1391
  }
1101
1392
  };
1102
1393
  function loadNotFoundRouteFromFilesystem(appDir) {
@@ -1229,7 +1520,8 @@ function loadAliasesFromTsconfig(projectRoot) {
1229
1520
  try {
1230
1521
  tsconfig = JSON.parse(fs11.readFileSync(tsconfigPath, "utf-8"));
1231
1522
  } catch (err) {
1232
- console.warn("[framework] Could not read tsconfig.json:", err);
1523
+ console.warn("\u26A0\uFE0F [framework] Could not read tsconfig.json:", err instanceof Error ? err.message : String(err));
1524
+ console.warn("\u{1F4A1} Using default path aliases. For custom aliases, ensure tsconfig.json is valid.");
1233
1525
  aliases["@app"] = path11.resolve(projectRoot, "app");
1234
1526
  return aliases;
1235
1527
  }
@@ -1283,7 +1575,7 @@ function copyStaticAssets(projectRoot, outDir) {
1283
1575
  }
1284
1576
  }
1285
1577
  }
1286
- function generateAssetManifest(outDir) {
1578
+ function generateAssetManifest(outDir, stats) {
1287
1579
  const manifest = {
1288
1580
  client: {
1289
1581
  js: "client.js",
@@ -1295,27 +1587,128 @@ function generateAssetManifest(outDir) {
1295
1587
  return manifest;
1296
1588
  }
1297
1589
  const files = fs11.readdirSync(outDir);
1298
- const clientJsMatch = files.find((f) => /^client\.[\w-]+\.js$/.test(f) || f === "client.js");
1299
- if (clientJsMatch) {
1300
- manifest.client.js = clientJsMatch;
1590
+ if (stats) {
1591
+ try {
1592
+ const statsJson = stats.toJson({
1593
+ all: false,
1594
+ entrypoints: true,
1595
+ assets: true,
1596
+ chunks: true,
1597
+ chunkRelations: true
1598
+ // Include chunk dependencies
1599
+ });
1600
+ const clientEntrypoint = statsJson.entrypoints?.client;
1601
+ if (clientEntrypoint?.assets) {
1602
+ const clientJsFiles = clientEntrypoint.assets.map((asset) => typeof asset === "string" ? asset : asset.name).filter((name) => name.endsWith(".js"));
1603
+ if (statsJson.chunks && clientEntrypoint.chunks) {
1604
+ const entrypointChunkIds = new Set(
1605
+ Array.isArray(clientEntrypoint.chunks) ? clientEntrypoint.chunks : [clientEntrypoint.chunks]
1606
+ );
1607
+ const dependencyChunks = [];
1608
+ for (const chunk of statsJson.chunks) {
1609
+ if (chunk.id && entrypointChunkIds.has(chunk.id)) {
1610
+ if (chunk.files) {
1611
+ const jsFiles = chunk.files.filter((f) => f.endsWith(".js")).filter((f) => !clientJsFiles.includes(f));
1612
+ dependencyChunks.push(...jsFiles);
1613
+ }
1614
+ }
1615
+ }
1616
+ const visitedChunkIds = new Set(entrypointChunkIds);
1617
+ const chunksToCheck = Array.from(entrypointChunkIds);
1618
+ while (chunksToCheck.length > 0) {
1619
+ const chunkId = chunksToCheck.shift();
1620
+ if (!chunkId) continue;
1621
+ const chunk = statsJson.chunks?.find((c) => c.id === chunkId);
1622
+ if (chunk?.children) {
1623
+ const children = Array.isArray(chunk.children) ? chunk.children : [chunk.children];
1624
+ for (const childId of children) {
1625
+ if (!visitedChunkIds.has(childId)) {
1626
+ visitedChunkIds.add(childId);
1627
+ chunksToCheck.push(childId);
1628
+ const childChunk = statsJson.chunks?.find((c) => c.id === childId);
1629
+ if (childChunk?.files) {
1630
+ const jsFiles = childChunk.files.filter((f) => f.endsWith(".js")).filter((f) => !clientJsFiles.includes(f) && !dependencyChunks.includes(f));
1631
+ dependencyChunks.push(...jsFiles);
1632
+ }
1633
+ }
1634
+ }
1635
+ }
1636
+ }
1637
+ if (dependencyChunks.length > 0) {
1638
+ clientJsFiles.splice(-1, 0, ...dependencyChunks);
1639
+ }
1640
+ }
1641
+ if (clientJsFiles.length > 0) {
1642
+ manifest.entrypoints = {
1643
+ client: clientJsFiles
1644
+ };
1645
+ manifest.client.js = clientJsFiles[clientJsFiles.length - 1];
1646
+ const clientCssFiles = clientEntrypoint.assets.map((asset) => typeof asset === "string" ? asset : asset.name).filter((name) => name.endsWith(".css"));
1647
+ if (clientCssFiles.length > 0) {
1648
+ manifest.client.css = clientCssFiles[0];
1649
+ }
1650
+ }
1651
+ }
1652
+ } catch (err) {
1653
+ console.warn("[framework] Failed to extract entrypoints from stats, falling back to file scanning:", err);
1654
+ }
1655
+ }
1656
+ if (!manifest.client.js) {
1657
+ const clientJsMatch = files.find((f) => /^client\.[\w-]+\.js$/.test(f) || f === "client.js");
1658
+ if (clientJsMatch) {
1659
+ manifest.client.js = clientJsMatch;
1660
+ }
1301
1661
  }
1302
- const clientCssMatch = files.find((f) => /^client\.[\w-]+\.css$/.test(f) || f === "client.css");
1303
- if (clientCssMatch) {
1304
- manifest.client.css = clientCssMatch;
1662
+ if (!manifest.client.css) {
1663
+ const clientCssMatch = files.find((f) => /^client\.[\w-]+\.css$/.test(f) || f === "client.css");
1664
+ if (clientCssMatch) {
1665
+ manifest.client.css = clientCssMatch;
1666
+ }
1305
1667
  }
1668
+ const sharedChunksToAdd = [];
1306
1669
  for (const file of files) {
1307
1670
  if (!file.endsWith(".js")) continue;
1308
1671
  if (file === manifest.client.js) continue;
1672
+ if (manifest.entrypoints?.client?.includes(file)) continue;
1309
1673
  const routeMatch = file.match(/^(route-[^.]+)(\.[\w-]+)?\.js$/);
1310
1674
  if (routeMatch) {
1311
1675
  const chunkName = routeMatch[1];
1312
1676
  manifest.chunks[chunkName] = file;
1313
1677
  continue;
1314
1678
  }
1679
+ const vendorMatch = file.match(/^(vendor)(\.[\w-]+)?\.js$/);
1680
+ if (vendorMatch) {
1681
+ const chunkName = vendorMatch[1];
1682
+ manifest.chunks[chunkName] = file;
1683
+ sharedChunksToAdd.push(file);
1684
+ continue;
1685
+ }
1686
+ const vendorCommonsMatch = file.match(/^(vendor-commons)(\.[\w-]+)?\.js$/);
1687
+ if (vendorCommonsMatch) {
1688
+ const chunkName = vendorCommonsMatch[1];
1689
+ manifest.chunks[chunkName] = file;
1690
+ sharedChunksToAdd.push(file);
1691
+ continue;
1692
+ }
1315
1693
  const numericMatch = file.match(/^(\d+)(\.[\w-]+)?\.js$/);
1316
1694
  if (numericMatch) {
1317
1695
  const chunkName = numericMatch[1];
1318
1696
  manifest.chunks[chunkName] = file;
1697
+ sharedChunksToAdd.push(file);
1698
+ continue;
1699
+ }
1700
+ }
1701
+ if (sharedChunksToAdd.length > 0 && manifest.entrypoints?.client) {
1702
+ const entrypoints = manifest.entrypoints.client;
1703
+ const mainEntry = entrypoints[entrypoints.length - 1];
1704
+ const uniqueShared = sharedChunksToAdd.filter((f) => !entrypoints.includes(f));
1705
+ entrypoints.splice(-1, 0, ...uniqueShared);
1706
+ if (entrypoints[entrypoints.length - 1] !== mainEntry) {
1707
+ const mainIndex = entrypoints.indexOf(mainEntry);
1708
+ if (mainIndex >= 0) {
1709
+ entrypoints.splice(mainIndex, 1);
1710
+ }
1711
+ entrypoints.push(mainEntry);
1319
1712
  }
1320
1713
  }
1321
1714
  return manifest;
@@ -1355,9 +1748,7 @@ function createClientConfig(projectRoot, mode) {
1355
1748
  const outDir = path12.join(buildDir, "client");
1356
1749
  const envPath2 = path12.join(projectRoot, ".env");
1357
1750
  if (fs12.existsSync(envPath2)) {
1358
- dotenv.config({
1359
- path: envPath2
1360
- });
1751
+ dotenv.config({ path: envPath2 });
1361
1752
  }
1362
1753
  const publicEnv = {};
1363
1754
  for (const [key, value] of Object.entries(process.env)) {
@@ -1416,14 +1807,57 @@ function createClientConfig(projectRoot, mode) {
1416
1807
  },
1417
1808
  plugins: [
1418
1809
  new rspack.DefinePlugin({
1419
- // Always define NODE_ENV, using mode as fallback if not set
1420
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || mode),
1810
+ // Use mode directly to ensure development mode is correctly set
1811
+ // This replaces process.env.NODE_ENV in the client bundle with the literal string value
1812
+ "process.env.NODE_ENV": JSON.stringify(mode),
1421
1813
  ...publicEnv
1422
1814
  }),
1423
1815
  new rspack.CssExtractRspackPlugin({
1424
1816
  filename: mode === "production" ? "client.[contenthash].css" : "client.css"
1425
1817
  })
1426
1818
  ],
1819
+ optimization: mode === "production" ? {
1820
+ usedExports: true,
1821
+ sideEffects: false,
1822
+ // More aggressive tree shaking - assume no side effects
1823
+ providedExports: true,
1824
+ concatenateModules: true,
1825
+ // Better for tree shaking
1826
+ minimize: true,
1827
+ removeEmptyChunks: true,
1828
+ mergeDuplicateChunks: true,
1829
+ // Improved code splitting: separate vendor chunks for better caching
1830
+ splitChunks: {
1831
+ chunks: "all",
1832
+ cacheGroups: {
1833
+ // Separate React/React-DOM into dedicated vendor chunk
1834
+ // This improves caching: React rarely changes, so users don't need to re-download it
1835
+ vendor: {
1836
+ test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
1837
+ name: "vendor",
1838
+ priority: 30,
1839
+ enforce: true,
1840
+ // Force separation even if only used once
1841
+ reuseExistingChunk: true
1842
+ },
1843
+ // Other node_modules dependencies in a separate chunk
1844
+ vendorCommons: {
1845
+ test: /[\\/]node_modules[\\/](?!(react|react-dom|scheduler)[\\/])/,
1846
+ name: "vendor-commons",
1847
+ priority: 10,
1848
+ minChunks: 2,
1849
+ // Only create if used in 2+ chunks
1850
+ reuseExistingChunk: true
1851
+ },
1852
+ // Default: shared application code (not in node_modules)
1853
+ default: {
1854
+ minChunks: 2,
1855
+ priority: 5,
1856
+ reuseExistingChunk: true
1857
+ }
1858
+ }
1859
+ }
1860
+ } : void 0,
1427
1861
  infrastructureLogging: {
1428
1862
  level: "error"
1429
1863
  },
@@ -1452,7 +1886,13 @@ function startClientBundler(projectRoot, mode = "development") {
1452
1886
  });
1453
1887
  compiler.watch({}, (err, stats) => {
1454
1888
  if (err) {
1455
- console.error("[framework][client] Rspack error:", err);
1889
+ console.error("\n\u274C [framework][client] Rspack compilation error:");
1890
+ console.error(err);
1891
+ console.error("\n\u{1F4A1} Suggestions:");
1892
+ console.error(" \u2022 Check for syntax errors in your code");
1893
+ console.error(" \u2022 Verify all imports are correct");
1894
+ console.error(" \u2022 Ensure all dependencies are installed");
1895
+ console.error(" \u2022 Try deleting .loly folder and rebuilding\n");
1456
1896
  isBuilding = false;
1457
1897
  lastBuildTime = Date.now();
1458
1898
  if (buildResolve) {
@@ -1463,17 +1903,20 @@ function startClientBundler(projectRoot, mode = "development") {
1463
1903
  return;
1464
1904
  }
1465
1905
  if (!stats) {
1906
+ console.warn("\u26A0\uFE0F [framework][client] Build completed but no stats available");
1466
1907
  isBuilding = false;
1467
1908
  lastBuildTime = Date.now();
1468
1909
  return;
1469
1910
  }
1470
1911
  if (stats.hasErrors()) {
1471
- console.error(
1472
- "[framework][client] Build with errors:\n",
1473
- stats.toString("errors-only")
1474
- );
1912
+ console.error("\n\u274C [framework][client] Build failed with errors:\n");
1913
+ console.error(stats.toString("errors-only"));
1914
+ console.error("\n\u{1F4A1} Common fixes:");
1915
+ console.error(" \u2022 Fix syntax errors shown above");
1916
+ console.error(" \u2022 Check for missing imports or dependencies");
1917
+ console.error(" \u2022 Verify TypeScript types are correct\n");
1475
1918
  } else {
1476
- console.log("[framework][client] \u2713 Client bundle rebuilt successfully");
1919
+ console.log("\u2705 [framework][client] Client bundle rebuilt successfully");
1477
1920
  }
1478
1921
  isBuilding = false;
1479
1922
  lastBuildTime = Date.now();
@@ -1508,23 +1951,33 @@ function buildClientBundle(projectRoot) {
1508
1951
  compiler.close(() => {
1509
1952
  });
1510
1953
  if (err) {
1511
- console.error("[framework][client] Build error:", err);
1954
+ console.error("\n\u274C [framework][client] Production build error:");
1955
+ console.error(err);
1956
+ console.error("\n\u{1F4A1} Suggestions:");
1957
+ console.error(" \u2022 Check for syntax errors in your code");
1958
+ console.error(" \u2022 Verify all imports are correct");
1959
+ console.error(" \u2022 Ensure all dependencies are installed");
1960
+ console.error(" \u2022 Review the error details above\n");
1512
1961
  return reject(err);
1513
1962
  }
1514
1963
  if (!stats) {
1515
- const error = new Error("No stats from Rspack");
1516
- console.error("[framework][client] Build error:", error);
1964
+ const error = new Error("No stats from Rspack - build may have failed silently");
1965
+ console.error("\n\u274C [framework][client] Build error:", error.message);
1966
+ console.error("\u{1F4A1} Try rebuilding or check Rspack configuration\n");
1517
1967
  return reject(error);
1518
1968
  }
1519
1969
  if (stats.hasErrors()) {
1520
- console.error(
1521
- "[framework][client] Build with errors:\n",
1522
- stats.toString("errors-only")
1523
- );
1524
- return reject(new Error("Client build failed"));
1970
+ console.error("\n\u274C [framework][client] Production build failed:\n");
1971
+ console.error(stats.toString("errors-only"));
1972
+ console.error("\n\u{1F4A1} Common fixes:");
1973
+ console.error(" \u2022 Fix syntax errors shown above");
1974
+ console.error(" \u2022 Check for missing imports or dependencies");
1975
+ console.error(" \u2022 Verify TypeScript types are correct");
1976
+ console.error(" \u2022 Review build configuration\n");
1977
+ return reject(new Error("Client build failed - see errors above"));
1525
1978
  }
1526
1979
  copyStaticAssets(projectRoot, outDir);
1527
- const assetManifest = generateAssetManifest(outDir);
1980
+ const assetManifest = generateAssetManifest(outDir, stats);
1528
1981
  const manifestPath = path13.join(projectRoot, BUILD_FOLDER_NAME, "asset-manifest.json");
1529
1982
  fs13.writeFileSync(manifestPath, JSON.stringify(assetManifest, null, 2), "utf-8");
1530
1983
  resolve3({ outDir });
@@ -1608,7 +2061,7 @@ var ReaddirpStream = class extends Readable {
1608
2061
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
1609
2062
  const statMethod = opts.lstat ? lstat : stat;
1610
2063
  if (wantBigintFsStats) {
1611
- this._stat = (path25) => statMethod(path25, { bigint: true });
2064
+ this._stat = (path28) => statMethod(path28, { bigint: true });
1612
2065
  } else {
1613
2066
  this._stat = statMethod;
1614
2067
  }
@@ -1633,8 +2086,8 @@ var ReaddirpStream = class extends Readable {
1633
2086
  const par = this.parent;
1634
2087
  const fil = par && par.files;
1635
2088
  if (fil && fil.length > 0) {
1636
- const { path: path25, depth } = par;
1637
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path25));
2089
+ const { path: path28, depth } = par;
2090
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path28));
1638
2091
  const awaited = await Promise.all(slice);
1639
2092
  for (const entry of awaited) {
1640
2093
  if (!entry)
@@ -1674,20 +2127,20 @@ var ReaddirpStream = class extends Readable {
1674
2127
  this.reading = false;
1675
2128
  }
1676
2129
  }
1677
- async _exploreDir(path25, depth) {
2130
+ async _exploreDir(path28, depth) {
1678
2131
  let files;
1679
2132
  try {
1680
- files = await readdir(path25, this._rdOptions);
2133
+ files = await readdir(path28, this._rdOptions);
1681
2134
  } catch (error) {
1682
2135
  this._onError(error);
1683
2136
  }
1684
- return { files, depth, path: path25 };
2137
+ return { files, depth, path: path28 };
1685
2138
  }
1686
- async _formatEntry(dirent, path25) {
2139
+ async _formatEntry(dirent, path28) {
1687
2140
  let entry;
1688
2141
  const basename3 = this._isDirent ? dirent.name : dirent;
1689
2142
  try {
1690
- const fullPath = presolve(pjoin(path25, basename3));
2143
+ const fullPath = presolve(pjoin(path28, basename3));
1691
2144
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
1692
2145
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
1693
2146
  } catch (err) {
@@ -2087,16 +2540,16 @@ var delFromSet = (main, prop, item) => {
2087
2540
  };
2088
2541
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
2089
2542
  var FsWatchInstances = /* @__PURE__ */ new Map();
2090
- function createFsWatchInstance(path25, options, listener, errHandler, emitRaw) {
2543
+ function createFsWatchInstance(path28, options, listener, errHandler, emitRaw) {
2091
2544
  const handleEvent = (rawEvent, evPath) => {
2092
- listener(path25);
2093
- emitRaw(rawEvent, evPath, { watchedPath: path25 });
2094
- if (evPath && path25 !== evPath) {
2095
- fsWatchBroadcast(sysPath.resolve(path25, evPath), KEY_LISTENERS, sysPath.join(path25, evPath));
2545
+ listener(path28);
2546
+ emitRaw(rawEvent, evPath, { watchedPath: path28 });
2547
+ if (evPath && path28 !== evPath) {
2548
+ fsWatchBroadcast(sysPath.resolve(path28, evPath), KEY_LISTENERS, sysPath.join(path28, evPath));
2096
2549
  }
2097
2550
  };
2098
2551
  try {
2099
- return fs_watch(path25, {
2552
+ return fs_watch(path28, {
2100
2553
  persistent: options.persistent
2101
2554
  }, handleEvent);
2102
2555
  } catch (error) {
@@ -2112,12 +2565,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
2112
2565
  listener(val1, val2, val3);
2113
2566
  });
2114
2567
  };
2115
- var setFsWatchListener = (path25, fullPath, options, handlers) => {
2568
+ var setFsWatchListener = (path28, fullPath, options, handlers) => {
2116
2569
  const { listener, errHandler, rawEmitter } = handlers;
2117
2570
  let cont = FsWatchInstances.get(fullPath);
2118
2571
  let watcher;
2119
2572
  if (!options.persistent) {
2120
- watcher = createFsWatchInstance(path25, options, listener, errHandler, rawEmitter);
2573
+ watcher = createFsWatchInstance(path28, options, listener, errHandler, rawEmitter);
2121
2574
  if (!watcher)
2122
2575
  return;
2123
2576
  return watcher.close.bind(watcher);
@@ -2128,7 +2581,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2128
2581
  addAndConvert(cont, KEY_RAW, rawEmitter);
2129
2582
  } else {
2130
2583
  watcher = createFsWatchInstance(
2131
- path25,
2584
+ path28,
2132
2585
  options,
2133
2586
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
2134
2587
  errHandler,
@@ -2143,7 +2596,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2143
2596
  cont.watcherUnusable = true;
2144
2597
  if (isWindows && error.code === "EPERM") {
2145
2598
  try {
2146
- const fd = await open(path25, "r");
2599
+ const fd = await open(path28, "r");
2147
2600
  await fd.close();
2148
2601
  broadcastErr(error);
2149
2602
  } catch (err) {
@@ -2174,7 +2627,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2174
2627
  };
2175
2628
  };
2176
2629
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
2177
- var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
2630
+ var setFsWatchFileListener = (path28, fullPath, options, handlers) => {
2178
2631
  const { listener, rawEmitter } = handlers;
2179
2632
  let cont = FsWatchFileInstances.get(fullPath);
2180
2633
  const copts = cont && cont.options;
@@ -2196,7 +2649,7 @@ var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
2196
2649
  });
2197
2650
  const currmtime = curr.mtimeMs;
2198
2651
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
2199
- foreach(cont.listeners, (listener2) => listener2(path25, curr));
2652
+ foreach(cont.listeners, (listener2) => listener2(path28, curr));
2200
2653
  }
2201
2654
  })
2202
2655
  };
@@ -2224,13 +2677,13 @@ var NodeFsHandler = class {
2224
2677
  * @param listener on fs change
2225
2678
  * @returns closer for the watcher instance
2226
2679
  */
2227
- _watchWithNodeFs(path25, listener) {
2680
+ _watchWithNodeFs(path28, listener) {
2228
2681
  const opts = this.fsw.options;
2229
- const directory = sysPath.dirname(path25);
2230
- const basename3 = sysPath.basename(path25);
2682
+ const directory = sysPath.dirname(path28);
2683
+ const basename3 = sysPath.basename(path28);
2231
2684
  const parent = this.fsw._getWatchedDir(directory);
2232
2685
  parent.add(basename3);
2233
- const absolutePath = sysPath.resolve(path25);
2686
+ const absolutePath = sysPath.resolve(path28);
2234
2687
  const options = {
2235
2688
  persistent: opts.persistent
2236
2689
  };
@@ -2240,12 +2693,12 @@ var NodeFsHandler = class {
2240
2693
  if (opts.usePolling) {
2241
2694
  const enableBin = opts.interval !== opts.binaryInterval;
2242
2695
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
2243
- closer = setFsWatchFileListener(path25, absolutePath, options, {
2696
+ closer = setFsWatchFileListener(path28, absolutePath, options, {
2244
2697
  listener,
2245
2698
  rawEmitter: this.fsw._emitRaw
2246
2699
  });
2247
2700
  } else {
2248
- closer = setFsWatchListener(path25, absolutePath, options, {
2701
+ closer = setFsWatchListener(path28, absolutePath, options, {
2249
2702
  listener,
2250
2703
  errHandler: this._boundHandleError,
2251
2704
  rawEmitter: this.fsw._emitRaw
@@ -2267,7 +2720,7 @@ var NodeFsHandler = class {
2267
2720
  let prevStats = stats;
2268
2721
  if (parent.has(basename3))
2269
2722
  return;
2270
- const listener = async (path25, newStats) => {
2723
+ const listener = async (path28, newStats) => {
2271
2724
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
2272
2725
  return;
2273
2726
  if (!newStats || newStats.mtimeMs === 0) {
@@ -2281,11 +2734,11 @@ var NodeFsHandler = class {
2281
2734
  this.fsw._emit(EV.CHANGE, file, newStats2);
2282
2735
  }
2283
2736
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
2284
- this.fsw._closeFile(path25);
2737
+ this.fsw._closeFile(path28);
2285
2738
  prevStats = newStats2;
2286
2739
  const closer2 = this._watchWithNodeFs(file, listener);
2287
2740
  if (closer2)
2288
- this.fsw._addPathCloser(path25, closer2);
2741
+ this.fsw._addPathCloser(path28, closer2);
2289
2742
  } else {
2290
2743
  prevStats = newStats2;
2291
2744
  }
@@ -2317,7 +2770,7 @@ var NodeFsHandler = class {
2317
2770
  * @param item basename of this item
2318
2771
  * @returns true if no more processing is needed for this entry.
2319
2772
  */
2320
- async _handleSymlink(entry, directory, path25, item) {
2773
+ async _handleSymlink(entry, directory, path28, item) {
2321
2774
  if (this.fsw.closed) {
2322
2775
  return;
2323
2776
  }
@@ -2327,7 +2780,7 @@ var NodeFsHandler = class {
2327
2780
  this.fsw._incrReadyCount();
2328
2781
  let linkPath;
2329
2782
  try {
2330
- linkPath = await fsrealpath(path25);
2783
+ linkPath = await fsrealpath(path28);
2331
2784
  } catch (e) {
2332
2785
  this.fsw._emitReady();
2333
2786
  return true;
@@ -2337,12 +2790,12 @@ var NodeFsHandler = class {
2337
2790
  if (dir.has(item)) {
2338
2791
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
2339
2792
  this.fsw._symlinkPaths.set(full, linkPath);
2340
- this.fsw._emit(EV.CHANGE, path25, entry.stats);
2793
+ this.fsw._emit(EV.CHANGE, path28, entry.stats);
2341
2794
  }
2342
2795
  } else {
2343
2796
  dir.add(item);
2344
2797
  this.fsw._symlinkPaths.set(full, linkPath);
2345
- this.fsw._emit(EV.ADD, path25, entry.stats);
2798
+ this.fsw._emit(EV.ADD, path28, entry.stats);
2346
2799
  }
2347
2800
  this.fsw._emitReady();
2348
2801
  return true;
@@ -2371,9 +2824,9 @@ var NodeFsHandler = class {
2371
2824
  return;
2372
2825
  }
2373
2826
  const item = entry.path;
2374
- let path25 = sysPath.join(directory, item);
2827
+ let path28 = sysPath.join(directory, item);
2375
2828
  current.add(item);
2376
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path25, item)) {
2829
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path28, item)) {
2377
2830
  return;
2378
2831
  }
2379
2832
  if (this.fsw.closed) {
@@ -2382,8 +2835,8 @@ var NodeFsHandler = class {
2382
2835
  }
2383
2836
  if (item === target || !target && !previous.has(item)) {
2384
2837
  this.fsw._incrReadyCount();
2385
- path25 = sysPath.join(dir, sysPath.relative(dir, path25));
2386
- this._addToNodeFs(path25, initialAdd, wh, depth + 1);
2838
+ path28 = sysPath.join(dir, sysPath.relative(dir, path28));
2839
+ this._addToNodeFs(path28, initialAdd, wh, depth + 1);
2387
2840
  }
2388
2841
  }).on(EV.ERROR, this._boundHandleError);
2389
2842
  return new Promise((resolve3, reject) => {
@@ -2452,13 +2905,13 @@ var NodeFsHandler = class {
2452
2905
  * @param depth Child path actually targeted for watch
2453
2906
  * @param target Child path actually targeted for watch
2454
2907
  */
2455
- async _addToNodeFs(path25, initialAdd, priorWh, depth, target) {
2908
+ async _addToNodeFs(path28, initialAdd, priorWh, depth, target) {
2456
2909
  const ready = this.fsw._emitReady;
2457
- if (this.fsw._isIgnored(path25) || this.fsw.closed) {
2910
+ if (this.fsw._isIgnored(path28) || this.fsw.closed) {
2458
2911
  ready();
2459
2912
  return false;
2460
2913
  }
2461
- const wh = this.fsw._getWatchHelpers(path25);
2914
+ const wh = this.fsw._getWatchHelpers(path28);
2462
2915
  if (priorWh) {
2463
2916
  wh.filterPath = (entry) => priorWh.filterPath(entry);
2464
2917
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -2474,8 +2927,8 @@ var NodeFsHandler = class {
2474
2927
  const follow = this.fsw.options.followSymlinks;
2475
2928
  let closer;
2476
2929
  if (stats.isDirectory()) {
2477
- const absPath = sysPath.resolve(path25);
2478
- const targetPath = follow ? await fsrealpath(path25) : path25;
2930
+ const absPath = sysPath.resolve(path28);
2931
+ const targetPath = follow ? await fsrealpath(path28) : path28;
2479
2932
  if (this.fsw.closed)
2480
2933
  return;
2481
2934
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -2485,29 +2938,29 @@ var NodeFsHandler = class {
2485
2938
  this.fsw._symlinkPaths.set(absPath, targetPath);
2486
2939
  }
2487
2940
  } else if (stats.isSymbolicLink()) {
2488
- const targetPath = follow ? await fsrealpath(path25) : path25;
2941
+ const targetPath = follow ? await fsrealpath(path28) : path28;
2489
2942
  if (this.fsw.closed)
2490
2943
  return;
2491
2944
  const parent = sysPath.dirname(wh.watchPath);
2492
2945
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
2493
2946
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
2494
- closer = await this._handleDir(parent, stats, initialAdd, depth, path25, wh, targetPath);
2947
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path28, wh, targetPath);
2495
2948
  if (this.fsw.closed)
2496
2949
  return;
2497
2950
  if (targetPath !== void 0) {
2498
- this.fsw._symlinkPaths.set(sysPath.resolve(path25), targetPath);
2951
+ this.fsw._symlinkPaths.set(sysPath.resolve(path28), targetPath);
2499
2952
  }
2500
2953
  } else {
2501
2954
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
2502
2955
  }
2503
2956
  ready();
2504
2957
  if (closer)
2505
- this.fsw._addPathCloser(path25, closer);
2958
+ this.fsw._addPathCloser(path28, closer);
2506
2959
  return false;
2507
2960
  } catch (error) {
2508
2961
  if (this.fsw._handleError(error)) {
2509
2962
  ready();
2510
- return path25;
2963
+ return path28;
2511
2964
  }
2512
2965
  }
2513
2966
  }
@@ -2550,26 +3003,26 @@ function createPattern(matcher) {
2550
3003
  }
2551
3004
  return () => false;
2552
3005
  }
2553
- function normalizePath(path25) {
2554
- if (typeof path25 !== "string")
3006
+ function normalizePath(path28) {
3007
+ if (typeof path28 !== "string")
2555
3008
  throw new Error("string expected");
2556
- path25 = sysPath2.normalize(path25);
2557
- path25 = path25.replace(/\\/g, "/");
3009
+ path28 = sysPath2.normalize(path28);
3010
+ path28 = path28.replace(/\\/g, "/");
2558
3011
  let prepend = false;
2559
- if (path25.startsWith("//"))
3012
+ if (path28.startsWith("//"))
2560
3013
  prepend = true;
2561
3014
  const DOUBLE_SLASH_RE2 = /\/\//;
2562
- while (path25.match(DOUBLE_SLASH_RE2))
2563
- path25 = path25.replace(DOUBLE_SLASH_RE2, "/");
3015
+ while (path28.match(DOUBLE_SLASH_RE2))
3016
+ path28 = path28.replace(DOUBLE_SLASH_RE2, "/");
2564
3017
  if (prepend)
2565
- path25 = "/" + path25;
2566
- return path25;
3018
+ path28 = "/" + path28;
3019
+ return path28;
2567
3020
  }
2568
3021
  function matchPatterns(patterns, testString, stats) {
2569
- const path25 = normalizePath(testString);
3022
+ const path28 = normalizePath(testString);
2570
3023
  for (let index = 0; index < patterns.length; index++) {
2571
3024
  const pattern = patterns[index];
2572
- if (pattern(path25, stats)) {
3025
+ if (pattern(path28, stats)) {
2573
3026
  return true;
2574
3027
  }
2575
3028
  }
@@ -2609,19 +3062,19 @@ var toUnix = (string) => {
2609
3062
  }
2610
3063
  return str;
2611
3064
  };
2612
- var normalizePathToUnix = (path25) => toUnix(sysPath2.normalize(toUnix(path25)));
2613
- var normalizeIgnored = (cwd = "") => (path25) => {
2614
- if (typeof path25 === "string") {
2615
- return normalizePathToUnix(sysPath2.isAbsolute(path25) ? path25 : sysPath2.join(cwd, path25));
3065
+ var normalizePathToUnix = (path28) => toUnix(sysPath2.normalize(toUnix(path28)));
3066
+ var normalizeIgnored = (cwd = "") => (path28) => {
3067
+ if (typeof path28 === "string") {
3068
+ return normalizePathToUnix(sysPath2.isAbsolute(path28) ? path28 : sysPath2.join(cwd, path28));
2616
3069
  } else {
2617
- return path25;
3070
+ return path28;
2618
3071
  }
2619
3072
  };
2620
- var getAbsolutePath = (path25, cwd) => {
2621
- if (sysPath2.isAbsolute(path25)) {
2622
- return path25;
3073
+ var getAbsolutePath = (path28, cwd) => {
3074
+ if (sysPath2.isAbsolute(path28)) {
3075
+ return path28;
2623
3076
  }
2624
- return sysPath2.join(cwd, path25);
3077
+ return sysPath2.join(cwd, path28);
2625
3078
  };
2626
3079
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
2627
3080
  var DirEntry = class {
@@ -2676,10 +3129,10 @@ var DirEntry = class {
2676
3129
  var STAT_METHOD_F = "stat";
2677
3130
  var STAT_METHOD_L = "lstat";
2678
3131
  var WatchHelper = class {
2679
- constructor(path25, follow, fsw) {
3132
+ constructor(path28, follow, fsw) {
2680
3133
  this.fsw = fsw;
2681
- const watchPath = path25;
2682
- this.path = path25 = path25.replace(REPLACER_RE, "");
3134
+ const watchPath = path28;
3135
+ this.path = path28 = path28.replace(REPLACER_RE, "");
2683
3136
  this.watchPath = watchPath;
2684
3137
  this.fullWatchPath = sysPath2.resolve(watchPath);
2685
3138
  this.dirParts = [];
@@ -2801,20 +3254,20 @@ var FSWatcher = class extends EventEmitter {
2801
3254
  this._closePromise = void 0;
2802
3255
  let paths = unifyPaths(paths_);
2803
3256
  if (cwd) {
2804
- paths = paths.map((path25) => {
2805
- const absPath = getAbsolutePath(path25, cwd);
3257
+ paths = paths.map((path28) => {
3258
+ const absPath = getAbsolutePath(path28, cwd);
2806
3259
  return absPath;
2807
3260
  });
2808
3261
  }
2809
- paths.forEach((path25) => {
2810
- this._removeIgnoredPath(path25);
3262
+ paths.forEach((path28) => {
3263
+ this._removeIgnoredPath(path28);
2811
3264
  });
2812
3265
  this._userIgnored = void 0;
2813
3266
  if (!this._readyCount)
2814
3267
  this._readyCount = 0;
2815
3268
  this._readyCount += paths.length;
2816
- Promise.all(paths.map(async (path25) => {
2817
- const res = await this._nodeFsHandler._addToNodeFs(path25, !_internal, void 0, 0, _origAdd);
3269
+ Promise.all(paths.map(async (path28) => {
3270
+ const res = await this._nodeFsHandler._addToNodeFs(path28, !_internal, void 0, 0, _origAdd);
2818
3271
  if (res)
2819
3272
  this._emitReady();
2820
3273
  return res;
@@ -2836,17 +3289,17 @@ var FSWatcher = class extends EventEmitter {
2836
3289
  return this;
2837
3290
  const paths = unifyPaths(paths_);
2838
3291
  const { cwd } = this.options;
2839
- paths.forEach((path25) => {
2840
- if (!sysPath2.isAbsolute(path25) && !this._closers.has(path25)) {
3292
+ paths.forEach((path28) => {
3293
+ if (!sysPath2.isAbsolute(path28) && !this._closers.has(path28)) {
2841
3294
  if (cwd)
2842
- path25 = sysPath2.join(cwd, path25);
2843
- path25 = sysPath2.resolve(path25);
3295
+ path28 = sysPath2.join(cwd, path28);
3296
+ path28 = sysPath2.resolve(path28);
2844
3297
  }
2845
- this._closePath(path25);
2846
- this._addIgnoredPath(path25);
2847
- if (this._watched.has(path25)) {
3298
+ this._closePath(path28);
3299
+ this._addIgnoredPath(path28);
3300
+ if (this._watched.has(path28)) {
2848
3301
  this._addIgnoredPath({
2849
- path: path25,
3302
+ path: path28,
2850
3303
  recursive: true
2851
3304
  });
2852
3305
  }
@@ -2910,38 +3363,38 @@ var FSWatcher = class extends EventEmitter {
2910
3363
  * @param stats arguments to be passed with event
2911
3364
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
2912
3365
  */
2913
- async _emit(event, path25, stats) {
3366
+ async _emit(event, path28, stats) {
2914
3367
  if (this.closed)
2915
3368
  return;
2916
3369
  const opts = this.options;
2917
3370
  if (isWindows)
2918
- path25 = sysPath2.normalize(path25);
3371
+ path28 = sysPath2.normalize(path28);
2919
3372
  if (opts.cwd)
2920
- path25 = sysPath2.relative(opts.cwd, path25);
2921
- const args = [path25];
3373
+ path28 = sysPath2.relative(opts.cwd, path28);
3374
+ const args = [path28];
2922
3375
  if (stats != null)
2923
3376
  args.push(stats);
2924
3377
  const awf = opts.awaitWriteFinish;
2925
3378
  let pw;
2926
- if (awf && (pw = this._pendingWrites.get(path25))) {
3379
+ if (awf && (pw = this._pendingWrites.get(path28))) {
2927
3380
  pw.lastChange = /* @__PURE__ */ new Date();
2928
3381
  return this;
2929
3382
  }
2930
3383
  if (opts.atomic) {
2931
3384
  if (event === EVENTS.UNLINK) {
2932
- this._pendingUnlinks.set(path25, [event, ...args]);
3385
+ this._pendingUnlinks.set(path28, [event, ...args]);
2933
3386
  setTimeout(() => {
2934
- this._pendingUnlinks.forEach((entry, path26) => {
3387
+ this._pendingUnlinks.forEach((entry, path29) => {
2935
3388
  this.emit(...entry);
2936
3389
  this.emit(EVENTS.ALL, ...entry);
2937
- this._pendingUnlinks.delete(path26);
3390
+ this._pendingUnlinks.delete(path29);
2938
3391
  });
2939
3392
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
2940
3393
  return this;
2941
3394
  }
2942
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path25)) {
3395
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path28)) {
2943
3396
  event = EVENTS.CHANGE;
2944
- this._pendingUnlinks.delete(path25);
3397
+ this._pendingUnlinks.delete(path28);
2945
3398
  }
2946
3399
  }
2947
3400
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -2959,16 +3412,16 @@ var FSWatcher = class extends EventEmitter {
2959
3412
  this.emitWithAll(event, args);
2960
3413
  }
2961
3414
  };
2962
- this._awaitWriteFinish(path25, awf.stabilityThreshold, event, awfEmit);
3415
+ this._awaitWriteFinish(path28, awf.stabilityThreshold, event, awfEmit);
2963
3416
  return this;
2964
3417
  }
2965
3418
  if (event === EVENTS.CHANGE) {
2966
- const isThrottled = !this._throttle(EVENTS.CHANGE, path25, 50);
3419
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path28, 50);
2967
3420
  if (isThrottled)
2968
3421
  return this;
2969
3422
  }
2970
3423
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
2971
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path25) : path25;
3424
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path28) : path28;
2972
3425
  let stats2;
2973
3426
  try {
2974
3427
  stats2 = await stat3(fullPath);
@@ -2999,23 +3452,23 @@ var FSWatcher = class extends EventEmitter {
2999
3452
  * @param timeout duration of time to suppress duplicate actions
3000
3453
  * @returns tracking object or false if action should be suppressed
3001
3454
  */
3002
- _throttle(actionType, path25, timeout) {
3455
+ _throttle(actionType, path28, timeout) {
3003
3456
  if (!this._throttled.has(actionType)) {
3004
3457
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
3005
3458
  }
3006
3459
  const action = this._throttled.get(actionType);
3007
3460
  if (!action)
3008
3461
  throw new Error("invalid throttle");
3009
- const actionPath = action.get(path25);
3462
+ const actionPath = action.get(path28);
3010
3463
  if (actionPath) {
3011
3464
  actionPath.count++;
3012
3465
  return false;
3013
3466
  }
3014
3467
  let timeoutObject;
3015
3468
  const clear = () => {
3016
- const item = action.get(path25);
3469
+ const item = action.get(path28);
3017
3470
  const count = item ? item.count : 0;
3018
- action.delete(path25);
3471
+ action.delete(path28);
3019
3472
  clearTimeout(timeoutObject);
3020
3473
  if (item)
3021
3474
  clearTimeout(item.timeoutObject);
@@ -3023,7 +3476,7 @@ var FSWatcher = class extends EventEmitter {
3023
3476
  };
3024
3477
  timeoutObject = setTimeout(clear, timeout);
3025
3478
  const thr = { timeoutObject, clear, count: 0 };
3026
- action.set(path25, thr);
3479
+ action.set(path28, thr);
3027
3480
  return thr;
3028
3481
  }
3029
3482
  _incrReadyCount() {
@@ -3037,44 +3490,44 @@ var FSWatcher = class extends EventEmitter {
3037
3490
  * @param event
3038
3491
  * @param awfEmit Callback to be called when ready for event to be emitted.
3039
3492
  */
3040
- _awaitWriteFinish(path25, threshold, event, awfEmit) {
3493
+ _awaitWriteFinish(path28, threshold, event, awfEmit) {
3041
3494
  const awf = this.options.awaitWriteFinish;
3042
3495
  if (typeof awf !== "object")
3043
3496
  return;
3044
3497
  const pollInterval = awf.pollInterval;
3045
3498
  let timeoutHandler;
3046
- let fullPath = path25;
3047
- if (this.options.cwd && !sysPath2.isAbsolute(path25)) {
3048
- fullPath = sysPath2.join(this.options.cwd, path25);
3499
+ let fullPath = path28;
3500
+ if (this.options.cwd && !sysPath2.isAbsolute(path28)) {
3501
+ fullPath = sysPath2.join(this.options.cwd, path28);
3049
3502
  }
3050
3503
  const now = /* @__PURE__ */ new Date();
3051
3504
  const writes = this._pendingWrites;
3052
3505
  function awaitWriteFinishFn(prevStat) {
3053
3506
  statcb(fullPath, (err, curStat) => {
3054
- if (err || !writes.has(path25)) {
3507
+ if (err || !writes.has(path28)) {
3055
3508
  if (err && err.code !== "ENOENT")
3056
3509
  awfEmit(err);
3057
3510
  return;
3058
3511
  }
3059
3512
  const now2 = Number(/* @__PURE__ */ new Date());
3060
3513
  if (prevStat && curStat.size !== prevStat.size) {
3061
- writes.get(path25).lastChange = now2;
3514
+ writes.get(path28).lastChange = now2;
3062
3515
  }
3063
- const pw = writes.get(path25);
3516
+ const pw = writes.get(path28);
3064
3517
  const df = now2 - pw.lastChange;
3065
3518
  if (df >= threshold) {
3066
- writes.delete(path25);
3519
+ writes.delete(path28);
3067
3520
  awfEmit(void 0, curStat);
3068
3521
  } else {
3069
3522
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
3070
3523
  }
3071
3524
  });
3072
3525
  }
3073
- if (!writes.has(path25)) {
3074
- writes.set(path25, {
3526
+ if (!writes.has(path28)) {
3527
+ writes.set(path28, {
3075
3528
  lastChange: now,
3076
3529
  cancelWait: () => {
3077
- writes.delete(path25);
3530
+ writes.delete(path28);
3078
3531
  clearTimeout(timeoutHandler);
3079
3532
  return event;
3080
3533
  }
@@ -3085,8 +3538,8 @@ var FSWatcher = class extends EventEmitter {
3085
3538
  /**
3086
3539
  * Determines whether user has asked to ignore this path.
3087
3540
  */
3088
- _isIgnored(path25, stats) {
3089
- if (this.options.atomic && DOT_RE.test(path25))
3541
+ _isIgnored(path28, stats) {
3542
+ if (this.options.atomic && DOT_RE.test(path28))
3090
3543
  return true;
3091
3544
  if (!this._userIgnored) {
3092
3545
  const { cwd } = this.options;
@@ -3096,17 +3549,17 @@ var FSWatcher = class extends EventEmitter {
3096
3549
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
3097
3550
  this._userIgnored = anymatch(list, void 0);
3098
3551
  }
3099
- return this._userIgnored(path25, stats);
3552
+ return this._userIgnored(path28, stats);
3100
3553
  }
3101
- _isntIgnored(path25, stat4) {
3102
- return !this._isIgnored(path25, stat4);
3554
+ _isntIgnored(path28, stat4) {
3555
+ return !this._isIgnored(path28, stat4);
3103
3556
  }
3104
3557
  /**
3105
3558
  * Provides a set of common helpers and properties relating to symlink handling.
3106
3559
  * @param path file or directory pattern being watched
3107
3560
  */
3108
- _getWatchHelpers(path25) {
3109
- return new WatchHelper(path25, this.options.followSymlinks, this);
3561
+ _getWatchHelpers(path28) {
3562
+ return new WatchHelper(path28, this.options.followSymlinks, this);
3110
3563
  }
3111
3564
  // Directory helpers
3112
3565
  // -----------------
@@ -3138,63 +3591,63 @@ var FSWatcher = class extends EventEmitter {
3138
3591
  * @param item base path of item/directory
3139
3592
  */
3140
3593
  _remove(directory, item, isDirectory) {
3141
- const path25 = sysPath2.join(directory, item);
3142
- const fullPath = sysPath2.resolve(path25);
3143
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path25) || this._watched.has(fullPath);
3144
- if (!this._throttle("remove", path25, 100))
3594
+ const path28 = sysPath2.join(directory, item);
3595
+ const fullPath = sysPath2.resolve(path28);
3596
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path28) || this._watched.has(fullPath);
3597
+ if (!this._throttle("remove", path28, 100))
3145
3598
  return;
3146
3599
  if (!isDirectory && this._watched.size === 1) {
3147
3600
  this.add(directory, item, true);
3148
3601
  }
3149
- const wp = this._getWatchedDir(path25);
3602
+ const wp = this._getWatchedDir(path28);
3150
3603
  const nestedDirectoryChildren = wp.getChildren();
3151
- nestedDirectoryChildren.forEach((nested) => this._remove(path25, nested));
3604
+ nestedDirectoryChildren.forEach((nested) => this._remove(path28, nested));
3152
3605
  const parent = this._getWatchedDir(directory);
3153
3606
  const wasTracked = parent.has(item);
3154
3607
  parent.remove(item);
3155
3608
  if (this._symlinkPaths.has(fullPath)) {
3156
3609
  this._symlinkPaths.delete(fullPath);
3157
3610
  }
3158
- let relPath = path25;
3611
+ let relPath = path28;
3159
3612
  if (this.options.cwd)
3160
- relPath = sysPath2.relative(this.options.cwd, path25);
3613
+ relPath = sysPath2.relative(this.options.cwd, path28);
3161
3614
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
3162
3615
  const event = this._pendingWrites.get(relPath).cancelWait();
3163
3616
  if (event === EVENTS.ADD)
3164
3617
  return;
3165
3618
  }
3166
- this._watched.delete(path25);
3619
+ this._watched.delete(path28);
3167
3620
  this._watched.delete(fullPath);
3168
3621
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
3169
- if (wasTracked && !this._isIgnored(path25))
3170
- this._emit(eventName, path25);
3171
- this._closePath(path25);
3622
+ if (wasTracked && !this._isIgnored(path28))
3623
+ this._emit(eventName, path28);
3624
+ this._closePath(path28);
3172
3625
  }
3173
3626
  /**
3174
3627
  * Closes all watchers for a path
3175
3628
  */
3176
- _closePath(path25) {
3177
- this._closeFile(path25);
3178
- const dir = sysPath2.dirname(path25);
3179
- this._getWatchedDir(dir).remove(sysPath2.basename(path25));
3629
+ _closePath(path28) {
3630
+ this._closeFile(path28);
3631
+ const dir = sysPath2.dirname(path28);
3632
+ this._getWatchedDir(dir).remove(sysPath2.basename(path28));
3180
3633
  }
3181
3634
  /**
3182
3635
  * Closes only file-specific watchers
3183
3636
  */
3184
- _closeFile(path25) {
3185
- const closers = this._closers.get(path25);
3637
+ _closeFile(path28) {
3638
+ const closers = this._closers.get(path28);
3186
3639
  if (!closers)
3187
3640
  return;
3188
3641
  closers.forEach((closer) => closer());
3189
- this._closers.delete(path25);
3642
+ this._closers.delete(path28);
3190
3643
  }
3191
- _addPathCloser(path25, closer) {
3644
+ _addPathCloser(path28, closer) {
3192
3645
  if (!closer)
3193
3646
  return;
3194
- let list = this._closers.get(path25);
3647
+ let list = this._closers.get(path28);
3195
3648
  if (!list) {
3196
3649
  list = [];
3197
- this._closers.set(path25, list);
3650
+ this._closers.set(path28, list);
3198
3651
  }
3199
3652
  list.push(closer);
3200
3653
  }
@@ -3229,54 +3682,132 @@ import path14 from "path";
3229
3682
  function setupHotReload({
3230
3683
  app,
3231
3684
  appDir,
3685
+ projectRoot,
3232
3686
  route = "/__fw/hot",
3233
3687
  waitForBuild,
3234
3688
  onFileChange
3235
3689
  }) {
3236
3690
  const clients = /* @__PURE__ */ new Set();
3237
3691
  app.get(route, (req, res) => {
3238
- res.setHeader("Content-Type", "text/event-stream");
3239
- res.setHeader("Cache-Control", "no-cache");
3692
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
3693
+ res.setHeader("Cache-Control", "no-cache, no-transform");
3240
3694
  res.setHeader("Connection", "keep-alive");
3241
- res.flushHeaders?.();
3242
- res.write(`event: ping
3243
- data: connected
3244
-
3245
- `);
3695
+ res.setHeader("X-Accel-Buffering", "no");
3696
+ if (res.flushHeaders) {
3697
+ res.flushHeaders();
3698
+ } else {
3699
+ res.writeHead(200, {
3700
+ "Content-Type": "text/event-stream; charset=utf-8",
3701
+ "Cache-Control": "no-cache, no-transform",
3702
+ "Connection": "keep-alive",
3703
+ "X-Accel-Buffering": "no"
3704
+ });
3705
+ }
3246
3706
  clients.add(res);
3707
+ const pingMessage = "event: ping\ndata: connected\n\n";
3708
+ try {
3709
+ res.write(pingMessage, "utf-8");
3710
+ } catch (error) {
3711
+ console.error(`[hot-reload-server] \u274C Error sending ping:`, error);
3712
+ clients.delete(res);
3713
+ }
3247
3714
  req.on("close", () => {
3248
3715
  clients.delete(res);
3249
3716
  });
3717
+ req.on("aborted", () => {
3718
+ clients.delete(res);
3719
+ });
3250
3720
  });
3251
- const watcher = esm_default.watch(appDir, {
3721
+ console.log(`[hot-reload-server] \u2705 SSE endpoint registered at ${route}`);
3722
+ const resolvedProjectRoot = projectRoot ? path14.resolve(projectRoot) : path14.dirname(path14.resolve(appDir));
3723
+ const watcher = esm_default.watch(resolvedProjectRoot, {
3252
3724
  ignoreInitial: true,
3253
- ignored: ["**/node_modules/**", `**/${BUILD_FOLDER_NAME}/**`, "**/.git/**"]
3725
+ ignored: [
3726
+ "**/node_modules/**",
3727
+ `**/${BUILD_FOLDER_NAME}/**`,
3728
+ "**/.loly/**",
3729
+ // Ignore build output directory completely
3730
+ "**/.git/**",
3731
+ "**/dist/**",
3732
+ "**/build/**",
3733
+ "**/.next/**",
3734
+ "**/.cache/**",
3735
+ "**/.turbo/**",
3736
+ "**/.swc/**",
3737
+ "**/coverage/**",
3738
+ // Ignore generated files that trigger unnecessary reloads
3739
+ "**/*.map",
3740
+ // Source maps
3741
+ "**/*.log",
3742
+ // Log files
3743
+ "**/.DS_Store",
3744
+ // macOS
3745
+ "**/Thumbs.db"
3746
+ // Windows
3747
+ ],
3748
+ persistent: true,
3749
+ ignorePermissionErrors: true,
3750
+ // Only watch relevant source files (TypeScript, JavaScript, CSS)
3751
+ // Filter out JSON files in build directories
3752
+ awaitWriteFinish: {
3753
+ stabilityThreshold: 150,
3754
+ // Wait 150ms after file change to trigger event (debounce)
3755
+ pollInterval: 50
3756
+ // Check every 50ms
3757
+ }
3254
3758
  });
3759
+ let broadcastTimeout = null;
3760
+ const BROADCAST_DEBOUNCE_MS = 300;
3255
3761
  async function broadcastReload(reason, filePath) {
3762
+ const normalizedPath = path14.normalize(filePath);
3763
+ if (normalizedPath.includes(BUILD_FOLDER_NAME) || normalizedPath.includes(".loly") || normalizedPath.endsWith(".map") || normalizedPath.endsWith(".log") || normalizedPath.includes("route-chunks.json") || normalizedPath.includes("routes-client.ts") || normalizedPath.includes("/client/route-")) {
3764
+ return;
3765
+ }
3256
3766
  const rel = path14.relative(appDir, filePath);
3257
3767
  console.log(`[hot-reload] ${reason}: ${rel}`);
3258
- if (onFileChange) {
3259
- try {
3260
- await onFileChange(filePath);
3261
- } catch (error) {
3262
- console.warn("[hot-reload] Error in onFileChange callback:", error);
3263
- }
3768
+ if (broadcastTimeout) {
3769
+ clearTimeout(broadcastTimeout);
3264
3770
  }
3265
- if (waitForBuild) {
3266
- try {
3267
- console.log("[hot-reload] Waiting for client bundle to finish...");
3268
- await waitForBuild();
3269
- console.log("[hot-reload] Client bundle ready, sending reload event");
3270
- } catch (error) {
3271
- console.warn("[hot-reload] Error waiting for build:", error);
3771
+ broadcastTimeout = setTimeout(async () => {
3772
+ if (onFileChange) {
3773
+ try {
3774
+ await onFileChange(filePath);
3775
+ } catch (error) {
3776
+ console.warn("[hot-reload] Error in onFileChange callback:", error);
3777
+ }
3272
3778
  }
3273
- }
3274
- for (const res of clients) {
3275
- res.write(`event: message
3779
+ if (waitForBuild) {
3780
+ try {
3781
+ console.log("[hot-reload] Waiting for client bundle to finish...");
3782
+ await waitForBuild();
3783
+ console.log("[hot-reload] Client bundle ready, sending reload event");
3784
+ } catch (error) {
3785
+ console.warn("[hot-reload] Error waiting for build:", error);
3786
+ }
3787
+ }
3788
+ const message = `event: message
3276
3789
  data: reload:${rel}
3277
3790
 
3278
- `);
3279
- }
3791
+ `;
3792
+ console.log(`[hot-reload-server] \u{1F4E4} Broadcasting reload event to ${clients.size} client(s)`);
3793
+ let sentCount = 0;
3794
+ for (const res of clients) {
3795
+ try {
3796
+ if (res.writableEnded || res.destroyed) {
3797
+ clients.delete(res);
3798
+ continue;
3799
+ }
3800
+ res.write(message, "utf-8");
3801
+ sentCount++;
3802
+ } catch (error) {
3803
+ console.error(`[hot-reload-server] \u2717 Error sending to client:`, error);
3804
+ clients.delete(res);
3805
+ }
3806
+ }
3807
+ if (sentCount > 0) {
3808
+ console.log(`[hot-reload-server] \u2705 Reload event sent to ${sentCount} client(s)`);
3809
+ }
3810
+ }, BROADCAST_DEBOUNCE_MS);
3280
3811
  }
3281
3812
  watcher.on("add", (filePath) => broadcastReload("add", filePath)).on("change", (filePath) => broadcastReload("change", filePath)).on("unlink", (filePath) => broadcastReload("unlink", filePath));
3282
3813
  }
@@ -3352,6 +3883,121 @@ function deepMerge(target, source) {
3352
3883
  }
3353
3884
  return result;
3354
3885
  }
3886
+ var ConfigValidationError = class extends Error {
3887
+ constructor(message, errors = []) {
3888
+ super(message);
3889
+ this.errors = errors;
3890
+ this.name = "ConfigValidationError";
3891
+ }
3892
+ };
3893
+ function validateConfig(config, projectRoot) {
3894
+ const errors = [];
3895
+ if (!config.directories.app || typeof config.directories.app !== "string") {
3896
+ errors.push("config.directories.app must be a non-empty string");
3897
+ } else {
3898
+ const appDir = path16.join(projectRoot, config.directories.app);
3899
+ if (!fs14.existsSync(appDir) && process.env.NODE_ENV !== "test") {
3900
+ errors.push(
3901
+ `App directory not found: ${config.directories.app}
3902
+ Expected at: ${appDir}
3903
+ \u{1F4A1} Suggestion: Create the directory or update config.directories.app`
3904
+ );
3905
+ }
3906
+ }
3907
+ if (!config.directories.build || typeof config.directories.build !== "string") {
3908
+ errors.push("config.directories.build must be a non-empty string");
3909
+ }
3910
+ if (!config.directories.static || typeof config.directories.static !== "string") {
3911
+ errors.push("config.directories.static must be a non-empty string");
3912
+ }
3913
+ const conventionKeys = ["page", "layout", "notFound", "error", "api"];
3914
+ for (const key of conventionKeys) {
3915
+ if (!config.conventions[key] || typeof config.conventions[key] !== "string") {
3916
+ errors.push(`config.conventions.${key} must be a non-empty string`);
3917
+ }
3918
+ }
3919
+ if (!["always", "never", "ignore"].includes(config.routing.trailingSlash)) {
3920
+ errors.push(
3921
+ `config.routing.trailingSlash must be 'always', 'never', or 'ignore'
3922
+ Received: ${JSON.stringify(config.routing.trailingSlash)}
3923
+ \u{1F4A1} Suggestion: Use one of the valid values: 'always' | 'never' | 'ignore'`
3924
+ );
3925
+ }
3926
+ if (typeof config.routing.caseSensitive !== "boolean") {
3927
+ errors.push("config.routing.caseSensitive must be a boolean");
3928
+ }
3929
+ if (typeof config.routing.basePath !== "string") {
3930
+ errors.push("config.routing.basePath must be a string");
3931
+ } else if (config.routing.basePath && !config.routing.basePath.startsWith("/")) {
3932
+ errors.push(
3933
+ `config.routing.basePath must start with '/' (if not empty)
3934
+ Received: ${JSON.stringify(config.routing.basePath)}
3935
+ \u{1F4A1} Suggestion: Use an empty string '' or a path starting with '/', e.g., '/api'`
3936
+ );
3937
+ }
3938
+ const validClientBundlers = ["rspack", "webpack", "vite"];
3939
+ if (!validClientBundlers.includes(config.build.clientBundler)) {
3940
+ errors.push(
3941
+ `config.build.clientBundler must be one of: ${validClientBundlers.join(", ")}
3942
+ Received: ${JSON.stringify(config.build.clientBundler)}`
3943
+ );
3944
+ }
3945
+ const validServerBundlers = ["esbuild", "tsup", "swc"];
3946
+ if (!validServerBundlers.includes(config.build.serverBundler)) {
3947
+ errors.push(
3948
+ `config.build.serverBundler must be one of: ${validServerBundlers.join(", ")}
3949
+ Received: ${JSON.stringify(config.build.serverBundler)}`
3950
+ );
3951
+ }
3952
+ if (!["cjs", "esm"].includes(config.build.outputFormat)) {
3953
+ errors.push(
3954
+ `config.build.outputFormat must be 'cjs' or 'esm'
3955
+ Received: ${JSON.stringify(config.build.outputFormat)}`
3956
+ );
3957
+ }
3958
+ const validAdapters = ["express", "fastify", "koa"];
3959
+ if (!validAdapters.includes(config.server.adapter)) {
3960
+ errors.push(
3961
+ `config.server.adapter must be one of: ${validAdapters.join(", ")}
3962
+ Received: ${JSON.stringify(config.server.adapter)}`
3963
+ );
3964
+ }
3965
+ if (typeof config.server.port !== "number" || config.server.port < 1 || config.server.port > 65535) {
3966
+ errors.push(
3967
+ `config.server.port must be a number between 1 and 65535
3968
+ Received: ${JSON.stringify(config.server.port)}`
3969
+ );
3970
+ }
3971
+ if (!config.server.host || typeof config.server.host !== "string") {
3972
+ errors.push("config.server.host must be a non-empty string");
3973
+ }
3974
+ const validFrameworks = ["react", "preact", "vue", "svelte"];
3975
+ if (!validFrameworks.includes(config.rendering.framework)) {
3976
+ errors.push(
3977
+ `config.rendering.framework must be one of: ${validFrameworks.join(", ")}
3978
+ Received: ${JSON.stringify(config.rendering.framework)}`
3979
+ );
3980
+ }
3981
+ if (typeof config.rendering.streaming !== "boolean") {
3982
+ errors.push("config.rendering.streaming must be a boolean");
3983
+ }
3984
+ if (typeof config.rendering.ssr !== "boolean") {
3985
+ errors.push("config.rendering.ssr must be a boolean");
3986
+ }
3987
+ if (typeof config.rendering.ssg !== "boolean") {
3988
+ errors.push("config.rendering.ssg must be a boolean");
3989
+ }
3990
+ if (errors.length > 0) {
3991
+ const errorMessage = [
3992
+ "\u274C Configuration validation failed:",
3993
+ "",
3994
+ ...errors.map((err, i) => `${i + 1}. ${err}`),
3995
+ "",
3996
+ "\u{1F4A1} Please check your loly.config.ts file and fix the errors above."
3997
+ ].join("\n");
3998
+ throw new ConfigValidationError(errorMessage, errors);
3999
+ }
4000
+ }
3355
4001
  function loadConfig(projectRoot) {
3356
4002
  const configFiles = [
3357
4003
  path16.join(projectRoot, "loly.config.ts"),
@@ -3359,6 +4005,7 @@ function loadConfig(projectRoot) {
3359
4005
  path16.join(projectRoot, "loly.config.json")
3360
4006
  ];
3361
4007
  let userConfig = {};
4008
+ let loadedConfigFile = null;
3362
4009
  for (const configFile of configFiles) {
3363
4010
  if (fs14.existsSync(configFile)) {
3364
4011
  try {
@@ -3372,16 +4019,31 @@ function loadConfig(projectRoot) {
3372
4019
  const mod = __require(configFile);
3373
4020
  userConfig = typeof mod.default === "function" ? mod.default(process.env.NODE_ENV) : mod.default || mod.config || mod;
3374
4021
  }
4022
+ loadedConfigFile = path16.relative(projectRoot, configFile);
3375
4023
  break;
3376
4024
  } catch (error) {
3377
- console.warn(`[framework] Failed to load config from ${configFile}:`, error);
4025
+ const errorMessage = error instanceof Error ? error.message : String(error);
4026
+ throw new ConfigValidationError(
4027
+ `Failed to load configuration from ${path16.relative(projectRoot, configFile)}:
4028
+ ${errorMessage}
4029
+ \u{1F4A1} Suggestion: Check that your config file exports a valid configuration object`
4030
+ );
3378
4031
  }
3379
4032
  }
3380
4033
  }
3381
4034
  const config = deepMerge(DEFAULT_CONFIG, userConfig);
3382
- const appDir = path16.join(projectRoot, config.directories.app);
3383
- if (!fs14.existsSync(appDir) && process.env.NODE_ENV !== "test") {
3384
- console.warn(`[framework] App directory not found: ${appDir}`);
4035
+ try {
4036
+ validateConfig(config, projectRoot);
4037
+ } catch (error) {
4038
+ if (error instanceof ConfigValidationError) {
4039
+ if (loadedConfigFile) {
4040
+ error.message = `[Configuration Error in ${loadedConfigFile}]
4041
+
4042
+ ${error.message}`;
4043
+ }
4044
+ throw error;
4045
+ }
4046
+ throw error;
3385
4047
  }
3386
4048
  return config;
3387
4049
  }
@@ -3398,14 +4060,14 @@ function getStaticDir(projectRoot, config) {
3398
4060
  // modules/server/setup.ts
3399
4061
  function setupServer(app, options) {
3400
4062
  const { projectRoot, appDir, isDev, config } = options;
3401
- const routeLoader = isDev ? new FilesystemRouteLoader(appDir) : new ManifestRouteLoader(projectRoot);
4063
+ const routeLoader = isDev ? new FilesystemRouteLoader(appDir, projectRoot) : new ManifestRouteLoader(projectRoot);
3402
4064
  if (isDev) {
3403
4065
  let getRoutes2 = function() {
3404
4066
  clearAppRequireCache(appDir);
3405
- const loader = new FilesystemRouteLoader(appDir);
4067
+ sharedLoader.invalidateCache();
3406
4068
  return {
3407
- routes: loader.loadRoutes(),
3408
- apiRoutes: loader.loadApiRoutes()
4069
+ routes: sharedLoader.loadRoutes(),
4070
+ apiRoutes: sharedLoader.loadApiRoutes()
3409
4071
  };
3410
4072
  };
3411
4073
  var getRoutes = getRoutes2;
@@ -3419,19 +4081,20 @@ function setupServer(app, options) {
3419
4081
  console.log(`[hot-reload] Cleared require cache for: ${rel}`);
3420
4082
  }
3421
4083
  if (isPageFile) {
3422
- const loader = new FilesystemRouteLoader(appDir);
4084
+ const loader = new FilesystemRouteLoader(appDir, projectRoot);
3423
4085
  const newRoutes = loader.loadRoutes();
3424
4086
  writeClientRoutesManifest(newRoutes, projectRoot);
3425
4087
  console.log("[hot-reload] Client routes manifest reloaded");
3426
4088
  }
3427
4089
  };
3428
- setupHotReload({ app, appDir, waitForBuild, onFileChange });
4090
+ setupHotReload({ app, appDir, projectRoot, waitForBuild, onFileChange });
3429
4091
  app.use("/static", express.static(outDir));
3430
4092
  const routes = routeLoader.loadRoutes();
3431
4093
  const wssRoutes = routeLoader.loadWssRoutes();
3432
4094
  const notFoundPage = routeLoader.loadNotFoundRoute();
3433
4095
  const errorPage = routeLoader.loadErrorRoute();
3434
4096
  writeClientRoutesManifest(routes, projectRoot);
4097
+ const sharedLoader = new FilesystemRouteLoader(appDir, projectRoot);
3435
4098
  return {
3436
4099
  routes,
3437
4100
  wssRoutes,
@@ -3519,104 +4182,24 @@ function sanitizeQuery(query) {
3519
4182
 
3520
4183
  // modules/server/middleware/rate-limit.ts
3521
4184
  import rateLimit from "express-rate-limit";
3522
- function createRateLimiter(config = {}) {
4185
+
4186
+ // modules/logger/index.ts
4187
+ import pino from "pino";
4188
+ function createLogger(options = {}) {
3523
4189
  const {
3524
- windowMs = 15 * 60 * 1e3,
3525
- // 15 minutes
3526
- max = 100,
3527
- // limit each IP to 100 requests per windowMs
3528
- message = "Too many requests from this IP, please try again later.",
3529
- standardHeaders = true,
3530
- legacyHeaders = false,
3531
- skipSuccessfulRequests = false,
3532
- skipFailedRequests = false
3533
- } = config;
3534
- return rateLimit({
3535
- windowMs,
3536
- max,
3537
- message: {
3538
- error: message
3539
- },
3540
- standardHeaders,
3541
- legacyHeaders,
3542
- skipSuccessfulRequests,
3543
- skipFailedRequests
3544
- });
3545
- }
3546
- var defaultRateLimiter = createRateLimiter({
3547
- windowMs: 15 * 60 * 1e3,
3548
- // 15 minutes
3549
- max: 100,
3550
- message: "Too many requests from this IP, please try again later."
3551
- });
3552
- var strictRateLimiter = createRateLimiter({
3553
- windowMs: 15 * 60 * 1e3,
3554
- // 15 minutes
3555
- max: 5,
3556
- message: "Too many authentication attempts, please try again later."
3557
- });
3558
- var lenientRateLimiter = createRateLimiter({
3559
- windowMs: 15 * 60 * 1e3,
3560
- // 15 minutes
3561
- max: 200,
3562
- message: "Too many requests from this IP, please try again later."
3563
- });
3564
-
3565
- // modules/server/middleware/auto-rate-limit.ts
3566
- function matchesStrictPattern(path25, patterns) {
3567
- for (const pattern of patterns) {
3568
- const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
3569
- const regex = new RegExp(`^${regexPattern}$`);
3570
- if (regex.test(path25)) {
3571
- return true;
3572
- }
3573
- }
3574
- return false;
3575
- }
3576
- function isRateLimiter(mw) {
3577
- if (!mw) return false;
3578
- if (mw === strictRateLimiter || mw === defaultRateLimiter || mw === lenientRateLimiter) {
3579
- return true;
3580
- }
3581
- if (typeof mw === "function" && mw.name && mw.name.includes("rateLimit")) {
3582
- return true;
3583
- }
3584
- if (mw && typeof mw === "function" && mw.skip || mw.resetKey) {
3585
- return true;
3586
- }
3587
- return false;
3588
- }
3589
- function getAutoRateLimiter(route, strictPatterns = []) {
3590
- const hasRateLimiter = route.middlewares?.some(isRateLimiter) || Object.values(route.methodMiddlewares || {}).some(
3591
- (mws) => mws?.some(isRateLimiter)
3592
- );
3593
- if (hasRateLimiter) {
3594
- return null;
3595
- }
3596
- if (strictPatterns.length > 0 && matchesStrictPattern(route.pattern, strictPatterns)) {
3597
- console.log(`[rate-limit] Applying strict rate limiter to route: ${route.pattern}`);
3598
- return strictRateLimiter;
3599
- }
3600
- return null;
3601
- }
3602
-
3603
- // modules/logger/index.ts
3604
- import pino from "pino";
3605
- function createLogger(options = {}) {
3606
- const {
3607
- level = process.env.LOG_LEVEL || (process.env.NODE_ENV === "development" ? "debug" : "info"),
3608
- enabled = process.env.LOG_ENABLED !== "false",
3609
- pretty = process.env.NODE_ENV === "development",
3610
- destination
3611
- } = options;
3612
- if (!enabled) {
3613
- return pino({ enabled: false });
3614
- }
3615
- const baseConfig = {
3616
- level,
3617
- base: {
3618
- name: "@lolyjs/core",
3619
- env: process.env.NODE_ENV || "development"
4190
+ level = process.env.LOG_LEVEL || (process.env.NODE_ENV === "development" ? "debug" : "info"),
4191
+ enabled = process.env.LOG_ENABLED !== "false",
4192
+ pretty = process.env.NODE_ENV === "development",
4193
+ destination
4194
+ } = options;
4195
+ if (!enabled) {
4196
+ return pino({ enabled: false });
4197
+ }
4198
+ const baseConfig = {
4199
+ level,
4200
+ base: {
4201
+ name: "@lolyjs/core",
4202
+ env: process.env.NODE_ENV || "development"
3620
4203
  },
3621
4204
  timestamp: pino.stdTimeFunctions.isoTime,
3622
4205
  formatters: {
@@ -3658,8 +4241,8 @@ function resetLogger() {
3658
4241
  loggerInstance = null;
3659
4242
  }
3660
4243
  var Logger = class _Logger {
3661
- constructor(logger4, context = {}) {
3662
- this.pino = logger4 || getLogger();
4244
+ constructor(logger5, context = {}) {
4245
+ this.pino = logger5 || getLogger();
3663
4246
  this.context = context;
3664
4247
  }
3665
4248
  /**
@@ -3736,12 +4319,12 @@ var DEFAULT_IGNORED_PATHS = [
3736
4319
  /^\/sockjs-node/
3737
4320
  // Hot reload websocket
3738
4321
  ];
3739
- function shouldIgnorePath(path25, ignoredPaths) {
4322
+ function shouldIgnorePath(path28, ignoredPaths) {
3740
4323
  return ignoredPaths.some((pattern) => {
3741
4324
  if (typeof pattern === "string") {
3742
- return path25 === pattern || path25.startsWith(pattern);
4325
+ return path28 === pattern || path28.startsWith(pattern);
3743
4326
  }
3744
- return pattern.test(path25);
4327
+ return pattern.test(path28);
3745
4328
  });
3746
4329
  }
3747
4330
  function requestLoggerMiddleware(options = {}) {
@@ -3792,6 +4375,151 @@ function getRequestLogger(req) {
3792
4375
  return req.logger || logger.child({ requestId: "unknown" });
3793
4376
  }
3794
4377
 
4378
+ // modules/server/middleware/rate-limit.ts
4379
+ var logger2 = createModuleLogger("rate-limit");
4380
+ function validateRateLimitConfig(config) {
4381
+ if (config.windowMs !== void 0 && (config.windowMs < 1e3 || !Number.isInteger(config.windowMs))) {
4382
+ throw new Error(
4383
+ `Invalid rateLimit.windowMs: ${config.windowMs}. Must be an integer >= 1000 (milliseconds)`
4384
+ );
4385
+ }
4386
+ if (config.max !== void 0 && (config.max < 1 || !Number.isInteger(config.max))) {
4387
+ throw new Error(
4388
+ `Invalid rateLimit.max: ${config.max}. Must be an integer >= 1`
4389
+ );
4390
+ }
4391
+ }
4392
+ function createRateLimiter(config = {}) {
4393
+ validateRateLimitConfig(config);
4394
+ const {
4395
+ windowMs = 15 * 60 * 1e3,
4396
+ // 15 minutes
4397
+ max = 100,
4398
+ // limit each IP to 100 requests per windowMs
4399
+ message = "Too many requests from this IP, please try again later.",
4400
+ standardHeaders = true,
4401
+ legacyHeaders = false,
4402
+ skipSuccessfulRequests = false,
4403
+ skipFailedRequests = false,
4404
+ keyGenerator,
4405
+ skip
4406
+ } = config;
4407
+ const limiter = rateLimit({
4408
+ windowMs,
4409
+ max,
4410
+ message: {
4411
+ error: message,
4412
+ retryAfter: Math.ceil(windowMs / 1e3)
4413
+ // seconds until retry
4414
+ },
4415
+ standardHeaders,
4416
+ legacyHeaders,
4417
+ skipSuccessfulRequests,
4418
+ skipFailedRequests,
4419
+ keyGenerator,
4420
+ skip
4421
+ });
4422
+ const wrappedLimiter = (req, res, next) => {
4423
+ limiter(req, res, (err) => {
4424
+ if (err && res.statusCode === 429) {
4425
+ const ip = req.ip || req.connection?.remoteAddress || "unknown";
4426
+ logger2.warn("Rate limit exceeded", {
4427
+ ip,
4428
+ path: req.path,
4429
+ method: req.method,
4430
+ limit: max,
4431
+ windowMs,
4432
+ retryAfter: Math.ceil(windowMs / 1e3)
4433
+ });
4434
+ }
4435
+ if (err) {
4436
+ return next(err);
4437
+ }
4438
+ next();
4439
+ });
4440
+ };
4441
+ Object.setPrototypeOf(wrappedLimiter, limiter);
4442
+ Object.assign(wrappedLimiter, limiter);
4443
+ return wrappedLimiter;
4444
+ }
4445
+ var defaultRateLimiter = createRateLimiter({
4446
+ windowMs: 15 * 60 * 1e3,
4447
+ // 15 minutes
4448
+ max: 100,
4449
+ message: "Too many requests from this IP, please try again later."
4450
+ });
4451
+ var strictRateLimiter = createRateLimiter({
4452
+ windowMs: 15 * 60 * 1e3,
4453
+ // 15 minutes
4454
+ max: 5,
4455
+ message: "Too many authentication attempts, please try again later."
4456
+ });
4457
+ var lenientRateLimiter = createRateLimiter({
4458
+ windowMs: 15 * 60 * 1e3,
4459
+ // 15 minutes
4460
+ max: 200,
4461
+ message: "Too many requests from this IP, please try again later."
4462
+ });
4463
+ function createRateLimiterFromConfig(config, useApiMax = false) {
4464
+ if (!config) return null;
4465
+ const max = useApiMax ? config.apiMax ?? config.max ?? 100 : config.max ?? 100;
4466
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
4467
+ return createRateLimiter({
4468
+ windowMs,
4469
+ max,
4470
+ message: `Too many requests from this IP, please try again after ${Math.ceil(windowMs / 1e3)} seconds.`
4471
+ });
4472
+ }
4473
+ function createStrictRateLimiterFromConfig(config) {
4474
+ if (!config || config.strictMax === void 0) {
4475
+ return strictRateLimiter;
4476
+ }
4477
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
4478
+ return createRateLimiter({
4479
+ windowMs,
4480
+ max: config.strictMax,
4481
+ message: `Too many authentication attempts, please try again after ${Math.ceil(windowMs / 1e3)} seconds.`
4482
+ });
4483
+ }
4484
+
4485
+ // modules/server/middleware/auto-rate-limit.ts
4486
+ function matchesStrictPattern(path28, patterns) {
4487
+ for (const pattern of patterns) {
4488
+ const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
4489
+ const regex = new RegExp(`^${regexPattern}$`);
4490
+ if (regex.test(path28)) {
4491
+ return true;
4492
+ }
4493
+ }
4494
+ return false;
4495
+ }
4496
+ function isRateLimiter(mw) {
4497
+ if (!mw) return false;
4498
+ if (mw === strictRateLimiter || mw === defaultRateLimiter || mw === lenientRateLimiter) {
4499
+ return true;
4500
+ }
4501
+ if (typeof mw === "function" && mw.name && mw.name.includes("rateLimit")) {
4502
+ return true;
4503
+ }
4504
+ if (mw && typeof mw === "function" && mw.skip || mw.resetKey) {
4505
+ return true;
4506
+ }
4507
+ return false;
4508
+ }
4509
+ function getAutoRateLimiter(route, strictPatterns = [], rateLimitConfig) {
4510
+ const hasRateLimiter = route.middlewares?.some(isRateLimiter) || Object.values(route.methodMiddlewares || {}).some(
4511
+ (mws) => mws?.some(isRateLimiter)
4512
+ );
4513
+ if (hasRateLimiter) {
4514
+ return null;
4515
+ }
4516
+ if (strictPatterns.length > 0 && matchesStrictPattern(route.pattern, strictPatterns)) {
4517
+ const limiter = rateLimitConfig ? createStrictRateLimiterFromConfig(rateLimitConfig) : strictRateLimiter;
4518
+ return limiter;
4519
+ }
4520
+ return null;
4521
+ }
4522
+
3795
4523
  // modules/server/handlers/api.ts
3796
4524
  async function handleApiRequest(options) {
3797
4525
  const { apiRoutes, urlPath, req, res, env = "dev" } = options;
@@ -3823,7 +4551,8 @@ async function handleApiRequest(options) {
3823
4551
  try {
3824
4552
  const autoRateLimiter = getAutoRateLimiter(
3825
4553
  route,
3826
- options.strictRateLimitPatterns
4554
+ options.strictRateLimitPatterns,
4555
+ options.rateLimitConfig
3827
4556
  );
3828
4557
  const reqLogger = getRequestLogger(req);
3829
4558
  if (autoRateLimiter) {
@@ -3835,26 +4564,46 @@ async function handleApiRequest(options) {
3835
4564
  const globalMws = route.middlewares ?? [];
3836
4565
  const perMethodMws = route.methodMiddlewares?.[method] ?? [];
3837
4566
  const chain = autoRateLimiter ? [autoRateLimiter, ...globalMws, ...perMethodMws] : [...globalMws, ...perMethodMws];
3838
- for (const mw of chain) {
3839
- const isExpressRateLimit = mw && typeof mw === "function" && (mw.skip || mw.resetKey || mw.name?.includes("rateLimit"));
3840
- if (isExpressRateLimit) {
3841
- await new Promise((resolve3, reject) => {
3842
- const next = (err) => {
3843
- if (err) reject(err);
3844
- else resolve3();
3845
- };
3846
- try {
3847
- const result = mw(req, res, next);
3848
- if (result && typeof result.then === "function") {
3849
- result.then(() => resolve3()).catch(reject);
4567
+ for (let i = 0; i < chain.length; i++) {
4568
+ const mw = chain[i];
4569
+ if (typeof mw !== "function") {
4570
+ reqLogger.warn("Invalid middleware in chain", {
4571
+ route: route.pattern,
4572
+ method,
4573
+ middlewareIndex: i,
4574
+ middlewareType: typeof mw
4575
+ });
4576
+ continue;
4577
+ }
4578
+ try {
4579
+ const isExpressRateLimit = mw.skip || mw.resetKey || mw.name?.includes("rateLimit");
4580
+ if (isExpressRateLimit) {
4581
+ await new Promise((resolve3, reject) => {
4582
+ const next = (err) => {
4583
+ if (err) reject(err);
4584
+ else resolve3();
4585
+ };
4586
+ try {
4587
+ const result = mw(req, res, next);
4588
+ if (result && typeof result.then === "function") {
4589
+ result.then(() => resolve3()).catch(reject);
4590
+ }
4591
+ } catch (err) {
4592
+ reject(err);
3850
4593
  }
3851
- } catch (err) {
3852
- reject(err);
3853
- }
4594
+ });
4595
+ } else {
4596
+ await Promise.resolve(mw(ctx, async () => {
4597
+ }));
4598
+ }
4599
+ } catch (error) {
4600
+ reqLogger.error("API middleware failed", error instanceof Error ? error : new Error(String(error)), {
4601
+ route: route.pattern,
4602
+ method,
4603
+ middlewareIndex: i,
4604
+ middlewareName: mw.name || "anonymous"
3854
4605
  });
3855
- } else {
3856
- await Promise.resolve(mw(ctx, async () => {
3857
- }));
4606
+ throw error;
3858
4607
  }
3859
4608
  if (res.headersSent) {
3860
4609
  return;
@@ -3905,42 +4654,289 @@ function createDocumentTree(options) {
3905
4654
  titleFallback,
3906
4655
  descriptionFallback,
3907
4656
  chunkHref,
4657
+ entrypointFiles = [],
3908
4658
  theme,
3909
4659
  clientJsPath = "/static/client.js",
3910
4660
  clientCssPath = "/static/client.css",
3911
- nonce
4661
+ nonce,
4662
+ includeInlineScripts = true
4663
+ // Default true - scripts inline in body for both SSR and SSG
3912
4664
  } = options;
3913
- const metaObj = meta ?? {};
3914
- const title = metaObj.title ?? titleFallback ?? "My Framework Dev";
3915
- const lang = metaObj.lang ?? "en";
3916
- const description = metaObj.description ?? descriptionFallback ?? "Demo Loly framework";
4665
+ const metaObj = meta ?? null;
4666
+ const title = metaObj?.title ?? titleFallback ?? "My Framework Dev";
4667
+ const lang = metaObj?.lang ?? "en";
4668
+ const description = metaObj?.description ?? descriptionFallback ?? "Demo Loly framework";
3917
4669
  const extraMetaTags = [];
4670
+ const linkTags = [];
3918
4671
  if (description) {
3919
4672
  extraMetaTags.push(
3920
4673
  React.createElement("meta", {
4674
+ key: "meta-description",
3921
4675
  name: "description",
3922
4676
  content: description
3923
4677
  })
3924
4678
  );
3925
4679
  }
3926
- if (Array.isArray(metaObj.metaTags)) {
3927
- for (const tag of metaObj.metaTags) {
4680
+ if (metaObj?.robots) {
4681
+ extraMetaTags.push(
4682
+ React.createElement("meta", {
4683
+ key: "meta-robots",
4684
+ name: "robots",
4685
+ content: metaObj.robots
4686
+ })
4687
+ );
4688
+ }
4689
+ if (metaObj?.themeColor) {
4690
+ extraMetaTags.push(
4691
+ React.createElement("meta", {
4692
+ key: "meta-theme-color",
4693
+ name: "theme-color",
4694
+ content: metaObj.themeColor
4695
+ })
4696
+ );
4697
+ }
4698
+ if (metaObj?.viewport) {
4699
+ extraMetaTags.push(
4700
+ React.createElement("meta", {
4701
+ key: "meta-viewport",
4702
+ name: "viewport",
4703
+ content: metaObj.viewport
4704
+ })
4705
+ );
4706
+ }
4707
+ if (metaObj?.canonical) {
4708
+ linkTags.push(
4709
+ React.createElement("link", {
4710
+ key: "link-canonical",
4711
+ rel: "canonical",
4712
+ href: metaObj.canonical
4713
+ })
4714
+ );
4715
+ }
4716
+ if (metaObj?.openGraph) {
4717
+ const og = metaObj.openGraph;
4718
+ if (og.title) {
4719
+ extraMetaTags.push(
4720
+ React.createElement("meta", {
4721
+ key: "og-title",
4722
+ property: "og:title",
4723
+ content: og.title
4724
+ })
4725
+ );
4726
+ }
4727
+ if (og.description) {
4728
+ extraMetaTags.push(
4729
+ React.createElement("meta", {
4730
+ key: "og-description",
4731
+ property: "og:description",
4732
+ content: og.description
4733
+ })
4734
+ );
4735
+ }
4736
+ if (og.type) {
4737
+ extraMetaTags.push(
4738
+ React.createElement("meta", {
4739
+ key: "og-type",
4740
+ property: "og:type",
4741
+ content: og.type
4742
+ })
4743
+ );
4744
+ }
4745
+ if (og.url) {
4746
+ extraMetaTags.push(
4747
+ React.createElement("meta", {
4748
+ key: "og-url",
4749
+ property: "og:url",
4750
+ content: og.url
4751
+ })
4752
+ );
4753
+ }
4754
+ if (og.image) {
4755
+ if (typeof og.image === "string") {
4756
+ extraMetaTags.push(
4757
+ React.createElement("meta", {
4758
+ key: "og-image",
4759
+ property: "og:image",
4760
+ content: og.image
4761
+ })
4762
+ );
4763
+ } else {
4764
+ extraMetaTags.push(
4765
+ React.createElement("meta", {
4766
+ key: "og-image",
4767
+ property: "og:image",
4768
+ content: og.image.url
4769
+ })
4770
+ );
4771
+ if (og.image.width) {
4772
+ extraMetaTags.push(
4773
+ React.createElement("meta", {
4774
+ key: "og-image-width",
4775
+ property: "og:image:width",
4776
+ content: String(og.image.width)
4777
+ })
4778
+ );
4779
+ }
4780
+ if (og.image.height) {
4781
+ extraMetaTags.push(
4782
+ React.createElement("meta", {
4783
+ key: "og-image-height",
4784
+ property: "og:image:height",
4785
+ content: String(og.image.height)
4786
+ })
4787
+ );
4788
+ }
4789
+ if (og.image.alt) {
4790
+ extraMetaTags.push(
4791
+ React.createElement("meta", {
4792
+ key: "og-image-alt",
4793
+ property: "og:image:alt",
4794
+ content: og.image.alt
4795
+ })
4796
+ );
4797
+ }
4798
+ }
4799
+ }
4800
+ if (og.siteName) {
4801
+ extraMetaTags.push(
4802
+ React.createElement("meta", {
4803
+ key: "og-site-name",
4804
+ property: "og:site_name",
4805
+ content: og.siteName
4806
+ })
4807
+ );
4808
+ }
4809
+ if (og.locale) {
4810
+ extraMetaTags.push(
4811
+ React.createElement("meta", {
4812
+ key: "og-locale",
4813
+ property: "og:locale",
4814
+ content: og.locale
4815
+ })
4816
+ );
4817
+ }
4818
+ }
4819
+ if (metaObj?.twitter) {
4820
+ const twitter = metaObj.twitter;
4821
+ if (twitter.card) {
3928
4822
  extraMetaTags.push(
3929
4823
  React.createElement("meta", {
4824
+ key: "twitter-card",
4825
+ name: "twitter:card",
4826
+ content: twitter.card
4827
+ })
4828
+ );
4829
+ }
4830
+ if (twitter.title) {
4831
+ extraMetaTags.push(
4832
+ React.createElement("meta", {
4833
+ key: "twitter-title",
4834
+ name: "twitter:title",
4835
+ content: twitter.title
4836
+ })
4837
+ );
4838
+ }
4839
+ if (twitter.description) {
4840
+ extraMetaTags.push(
4841
+ React.createElement("meta", {
4842
+ key: "twitter-description",
4843
+ name: "twitter:description",
4844
+ content: twitter.description
4845
+ })
4846
+ );
4847
+ }
4848
+ if (twitter.image) {
4849
+ extraMetaTags.push(
4850
+ React.createElement("meta", {
4851
+ key: "twitter-image",
4852
+ name: "twitter:image",
4853
+ content: twitter.image
4854
+ })
4855
+ );
4856
+ }
4857
+ if (twitter.imageAlt) {
4858
+ extraMetaTags.push(
4859
+ React.createElement("meta", {
4860
+ key: "twitter-image-alt",
4861
+ name: "twitter:image:alt",
4862
+ content: twitter.imageAlt
4863
+ })
4864
+ );
4865
+ }
4866
+ if (twitter.site) {
4867
+ extraMetaTags.push(
4868
+ React.createElement("meta", {
4869
+ key: "twitter-site",
4870
+ name: "twitter:site",
4871
+ content: twitter.site
4872
+ })
4873
+ );
4874
+ }
4875
+ if (twitter.creator) {
4876
+ extraMetaTags.push(
4877
+ React.createElement("meta", {
4878
+ key: "twitter-creator",
4879
+ name: "twitter:creator",
4880
+ content: twitter.creator
4881
+ })
4882
+ );
4883
+ }
4884
+ }
4885
+ if (metaObj?.metaTags && Array.isArray(metaObj.metaTags)) {
4886
+ metaObj.metaTags.forEach((tag, index) => {
4887
+ extraMetaTags.push(
4888
+ React.createElement("meta", {
4889
+ key: `meta-custom-${index}`,
3930
4890
  name: tag.name,
3931
4891
  property: tag.property,
4892
+ httpEquiv: tag.httpEquiv,
3932
4893
  content: tag.content
3933
4894
  })
3934
4895
  );
3935
- }
4896
+ });
4897
+ }
4898
+ if (metaObj?.links && Array.isArray(metaObj.links)) {
4899
+ metaObj.links.forEach((link, index) => {
4900
+ linkTags.push(
4901
+ React.createElement("link", {
4902
+ key: `link-custom-${index}`,
4903
+ rel: link.rel,
4904
+ href: link.href,
4905
+ as: link.as,
4906
+ crossOrigin: link.crossorigin,
4907
+ type: link.type
4908
+ })
4909
+ );
4910
+ });
4911
+ }
4912
+ const serialized = JSON.stringify({
4913
+ ...initialData,
4914
+ theme
4915
+ });
4916
+ const routerSerialized = JSON.stringify({
4917
+ ...routerData
4918
+ });
4919
+ const bodyChildren = [
4920
+ React.createElement("div", { id: APP_CONTAINER_ID }, appTree)
4921
+ ];
4922
+ if (includeInlineScripts) {
4923
+ bodyChildren.push(
4924
+ React.createElement("script", {
4925
+ key: "initial-data",
4926
+ nonce,
4927
+ dangerouslySetInnerHTML: {
4928
+ __html: `window.${WINDOW_DATA_KEY} = ${serialized};`
4929
+ }
4930
+ }),
4931
+ React.createElement("script", {
4932
+ key: "router-data",
4933
+ nonce,
4934
+ dangerouslySetInnerHTML: {
4935
+ __html: `window.${ROUTER_DATA_KEY} = ${routerSerialized};`
4936
+ }
4937
+ })
4938
+ );
3936
4939
  }
3937
- const serialized = JSON.stringify({
3938
- ...initialData,
3939
- theme
3940
- });
3941
- const routerSerialized = JSON.stringify({
3942
- ...routerData
3943
- });
3944
4940
  const documentTree = React.createElement(
3945
4941
  "html",
3946
4942
  { lang },
@@ -3949,13 +4945,25 @@ function createDocumentTree(options) {
3949
4945
  null,
3950
4946
  React.createElement("meta", { charSet: "utf-8" }),
3951
4947
  React.createElement("title", null, title),
4948
+ // Viewport: use custom if provided, otherwise default
3952
4949
  React.createElement("meta", {
3953
4950
  name: "viewport",
3954
- content: "width=device-width, initial-scale=1"
4951
+ content: metaObj?.viewport ?? "width=device-width, initial-scale=1"
3955
4952
  }),
3956
4953
  ...extraMetaTags,
4954
+ ...linkTags,
4955
+ ...entrypointFiles.length > 0 ? entrypointFiles.slice(0, -1).map(
4956
+ (file) => React.createElement("link", {
4957
+ key: `preload-${file}`,
4958
+ rel: "preload",
4959
+ href: file,
4960
+ as: "script"
4961
+ })
4962
+ ) : [],
4963
+ // Preload route-specific chunk if available
3957
4964
  chunkHref && React.createElement("link", {
3958
- rel: "modulepreload",
4965
+ key: `preload-${chunkHref}`,
4966
+ rel: "preload",
3959
4967
  href: chunkHref,
3960
4968
  as: "script"
3961
4969
  }),
@@ -3968,33 +4976,34 @@ function createDocumentTree(options) {
3968
4976
  rel: "stylesheet",
3969
4977
  href: clientCssPath
3970
4978
  }),
3971
- React.createElement("script", {
3972
- src: clientJsPath,
3973
- defer: true
3974
- })
4979
+ ...entrypointFiles.length > 0 ? entrypointFiles.map(
4980
+ (file) => React.createElement("script", {
4981
+ key: file,
4982
+ src: file,
4983
+ defer: true,
4984
+ nonce
4985
+ // CSP nonce for external scripts
4986
+ })
4987
+ ) : [
4988
+ React.createElement("script", {
4989
+ key: "client",
4990
+ src: clientJsPath,
4991
+ defer: true,
4992
+ nonce
4993
+ // CSP nonce for external scripts
4994
+ })
4995
+ ]
3975
4996
  ),
3976
4997
  React.createElement(
3977
4998
  "body",
3978
4999
  {
3979
5000
  style: { margin: 0 },
3980
- className: [initialData.className, theme].filter(Boolean).join(" "),
5001
+ className: [initialData.className || "", theme].filter(Boolean).join(" "),
3981
5002
  suppressHydrationWarning: true
3982
5003
  // Allow theme class to differ between server and client initially
3983
5004
  },
3984
- React.createElement("div", { id: APP_CONTAINER_ID }, appTree)
3985
- ),
3986
- React.createElement("script", {
3987
- nonce,
3988
- dangerouslySetInnerHTML: {
3989
- __html: `window.${WINDOW_DATA_KEY} = ${serialized};`
3990
- }
3991
- }),
3992
- React.createElement("script", {
3993
- nonce,
3994
- dangerouslySetInnerHTML: {
3995
- __html: `window.${ROUTER_DATA_KEY} = ${routerSerialized};`
3996
- }
3997
- })
5005
+ ...bodyChildren
5006
+ )
3998
5007
  );
3999
5008
  return documentTree;
4000
5009
  }
@@ -4010,7 +5019,7 @@ function buildInitialData(urlPath, params, loaderResult) {
4010
5019
  params,
4011
5020
  props,
4012
5021
  metadata: loaderResult.metadata ?? null,
4013
- className: loaderResult.className ?? "",
5022
+ className: loaderResult.className,
4014
5023
  error: false,
4015
5024
  notFound: false
4016
5025
  };
@@ -4026,12 +5035,25 @@ var buildRouterData = (req) => {
4026
5035
  };
4027
5036
 
4028
5037
  // modules/server/handlers/middleware.ts
5038
+ import path18 from "path";
4029
5039
  async function runRouteMiddlewares(route, ctx) {
4030
- for (const mw of route.middlewares) {
4031
- await Promise.resolve(
4032
- mw(ctx, async () => {
4033
- })
4034
- );
5040
+ for (let i = 0; i < route.middlewares.length; i++) {
5041
+ const mw = route.middlewares[i];
5042
+ try {
5043
+ await Promise.resolve(
5044
+ mw(ctx, async () => {
5045
+ })
5046
+ );
5047
+ } catch (error) {
5048
+ const reqLogger = getRequestLogger(ctx.req);
5049
+ const relativePath = route.pageFile ? path18.relative(process.cwd(), route.pageFile) : route.pattern;
5050
+ reqLogger.error("Route middleware failed", error instanceof Error ? error : new Error(String(error)), {
5051
+ route: route.pattern,
5052
+ middlewareIndex: i,
5053
+ pageFile: relativePath
5054
+ });
5055
+ throw error;
5056
+ }
4035
5057
  if (ctx.res.headersSent) {
4036
5058
  return;
4037
5059
  }
@@ -4039,11 +5061,57 @@ async function runRouteMiddlewares(route, ctx) {
4039
5061
  }
4040
5062
 
4041
5063
  // modules/server/handlers/server-hook.ts
5064
+ import path19 from "path";
5065
+ function createServerHookErrorMessage(error, hookType, routePattern, filePath) {
5066
+ const errorMessage = error instanceof Error ? error.message : String(error);
5067
+ const errorStack = error instanceof Error ? error.stack : void 0;
5068
+ let message = `[${hookType.toUpperCase()} SERVER HOOK ERROR]
5069
+ `;
5070
+ message += `Route: ${routePattern}
5071
+ `;
5072
+ if (filePath) {
5073
+ const relativePath = path19.relative(process.cwd(), filePath);
5074
+ message += `File: ${relativePath}
5075
+ `;
5076
+ }
5077
+ message += `Error: ${errorMessage}
5078
+ `;
5079
+ if (errorMessage.includes("Cannot find module")) {
5080
+ message += `
5081
+ \u{1F4A1} Suggestion: Check that all imports in your ${hookType}.server.hook.ts are correct.
5082
+ `;
5083
+ } else if (errorMessage.includes("is not defined") || errorMessage.includes("Cannot read property")) {
5084
+ message += `
5085
+ \u{1F4A1} Suggestion: Verify that all variables and properties exist in your ${hookType}.server.hook.ts.
5086
+ `;
5087
+ } else if (errorMessage.includes("async") || errorMessage.includes("await")) {
5088
+ message += `
5089
+ \u{1F4A1} Suggestion: Make sure getServerSideProps is an async function and all promises are awaited.
5090
+ `;
5091
+ }
5092
+ if (errorStack) {
5093
+ message += `
5094
+ Stack trace:
5095
+ ${errorStack}`;
5096
+ }
5097
+ return message;
5098
+ }
4042
5099
  async function runRouteServerHook(route, ctx) {
4043
5100
  if (!route.loader) {
4044
5101
  return { props: {} };
4045
5102
  }
4046
- return await route.loader(ctx);
5103
+ try {
5104
+ return await route.loader(ctx);
5105
+ } catch (error) {
5106
+ const detailedError = new Error(
5107
+ createServerHookErrorMessage(error, "page", route.pattern, route.pageFile)
5108
+ );
5109
+ if (error instanceof Error && error.stack) {
5110
+ detailedError.stack = error.stack;
5111
+ }
5112
+ detailedError.originalError = error;
5113
+ throw detailedError;
5114
+ }
4047
5115
  }
4048
5116
 
4049
5117
  // modules/server/handlers/response.ts
@@ -4084,26 +5152,26 @@ function handleNotFound(res, urlPath) {
4084
5152
 
4085
5153
  // modules/server/handlers/ssg.ts
4086
5154
  import fs15 from "fs";
4087
- import path18 from "path";
4088
- var logger2 = createModuleLogger("ssg");
5155
+ import path20 from "path";
5156
+ var logger3 = createModuleLogger("ssg");
4089
5157
  function getSsgDirForPath(baseDir, urlPath) {
4090
5158
  const clean = urlPath === "/" ? "" : urlPath.replace(/^\/+/, "");
4091
- return path18.join(baseDir, clean);
5159
+ return path20.join(baseDir, clean);
4092
5160
  }
4093
5161
  function getSsgHtmlPath(baseDir, urlPath) {
4094
5162
  const dir = getSsgDirForPath(baseDir, urlPath);
4095
- return path18.join(dir, "index.html");
5163
+ return path20.join(dir, "index.html");
4096
5164
  }
4097
5165
  function getSsgDataPath(baseDir, urlPath) {
4098
5166
  const dir = getSsgDirForPath(baseDir, urlPath);
4099
- return path18.join(dir, "data.json");
5167
+ return path20.join(dir, "data.json");
4100
5168
  }
4101
5169
  function tryServeSsgHtml(res, ssgOutDir, urlPath) {
4102
5170
  const ssgHtmlPath = getSsgHtmlPath(ssgOutDir, urlPath);
4103
5171
  if (!fs15.existsSync(ssgHtmlPath)) {
4104
5172
  return false;
4105
5173
  }
4106
- logger2.info("Serving SSG HTML", { urlPath, ssgHtmlPath });
5174
+ logger3.info("Serving SSG HTML", { urlPath, ssgHtmlPath });
4107
5175
  res.setHeader(
4108
5176
  "Content-Security-Policy",
4109
5177
  "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; font-src 'self' data: https://fonts.gstatic.com; object-src 'none'; media-src 'self' https:; frame-src 'none';"
@@ -4125,13 +5193,43 @@ function tryServeSsgData(res, ssgOutDir, urlPath) {
4125
5193
  res.status(200).end(raw);
4126
5194
  return true;
4127
5195
  } catch (err) {
4128
- logger2.error("Error reading SSG data", err, { urlPath, ssgDataPath });
5196
+ logger3.error("Error reading SSG data", err, { urlPath, ssgDataPath });
4129
5197
  return false;
4130
5198
  }
4131
5199
  }
4132
5200
 
4133
5201
  // modules/server/handlers/pages.ts
4134
5202
  init_globals();
5203
+ import path21 from "path";
5204
+ function mergeMetadata(base, override) {
5205
+ if (!base && !override) return null;
5206
+ if (!base) return override;
5207
+ if (!override) return base;
5208
+ return {
5209
+ // Simple fields: override wins
5210
+ title: override.title ?? base.title,
5211
+ description: override.description ?? base.description,
5212
+ lang: override.lang ?? base.lang,
5213
+ canonical: override.canonical ?? base.canonical,
5214
+ robots: override.robots ?? base.robots,
5215
+ themeColor: override.themeColor ?? base.themeColor,
5216
+ viewport: override.viewport ?? base.viewport,
5217
+ // Nested objects: shallow merge (override wins for each field)
5218
+ openGraph: override.openGraph ? {
5219
+ ...base.openGraph,
5220
+ ...override.openGraph,
5221
+ // For image, if override has image, use it entirely (don't merge)
5222
+ image: override.openGraph.image ?? base.openGraph?.image
5223
+ } : base.openGraph,
5224
+ twitter: override.twitter ? {
5225
+ ...base.twitter,
5226
+ ...override.twitter
5227
+ } : base.twitter,
5228
+ // Arrays: override replaces base entirely (not merged)
5229
+ metaTags: override.metaTags ?? base.metaTags,
5230
+ links: override.links ?? base.links
5231
+ };
5232
+ }
4135
5233
  function isDataRequest(req) {
4136
5234
  return req.query && req.query.__fw_data === "1" || req.headers["x-fw-data"] === "1";
4137
5235
  }
@@ -4208,9 +5306,13 @@ async function handlePageRequestInternal(options) {
4208
5306
  }
4209
5307
  } catch (error) {
4210
5308
  const reqLogger2 = getRequestLogger(req);
4211
- reqLogger2.warn(`Layout server hook ${i} failed for not-found`, {
4212
- error,
4213
- layoutFile: notFoundPage.layoutFiles[i]
5309
+ const layoutFile = notFoundPage.layoutFiles[i];
5310
+ const relativeLayoutPath = layoutFile ? path21.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5311
+ reqLogger2.warn("Layout server hook failed for not-found page", {
5312
+ error: error instanceof Error ? error.message : String(error),
5313
+ stack: error instanceof Error ? error.stack : void 0,
5314
+ layoutFile: relativeLayoutPath,
5315
+ layoutIndex: i
4214
5316
  });
4215
5317
  }
4216
5318
  }
@@ -4232,6 +5334,10 @@ async function handlePageRequestInternal(options) {
4232
5334
  const appTree2 = buildAppTree(notFoundPage, {}, initialData2.props);
4233
5335
  initialData2.notFound = true;
4234
5336
  const nonce2 = res.locals.nonce || void 0;
5337
+ const entrypointFiles2 = [];
5338
+ if (assetManifest?.entrypoints?.client) {
5339
+ entrypointFiles2.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5340
+ }
4235
5341
  const documentTree2 = createDocumentTree({
4236
5342
  appTree: appTree2,
4237
5343
  initialData: initialData2,
@@ -4240,6 +5346,7 @@ async function handlePageRequestInternal(options) {
4240
5346
  titleFallback: "Not found",
4241
5347
  descriptionFallback: "Loly demo",
4242
5348
  chunkHref: null,
5349
+ entrypointFiles: entrypointFiles2,
4243
5350
  theme,
4244
5351
  clientJsPath,
4245
5352
  clientCssPath,
@@ -4292,6 +5399,7 @@ async function handlePageRequestInternal(options) {
4292
5399
  return;
4293
5400
  }
4294
5401
  const layoutProps = {};
5402
+ const layoutMetadata = [];
4295
5403
  const reqLogger = getRequestLogger(req);
4296
5404
  if (route.layoutServerHooks && route.layoutServerHooks.length > 0) {
4297
5405
  for (let i = 0; i < route.layoutServerHooks.length; i++) {
@@ -4302,11 +5410,19 @@ async function handlePageRequestInternal(options) {
4302
5410
  if (layoutResult.props) {
4303
5411
  Object.assign(layoutProps, layoutResult.props);
4304
5412
  }
5413
+ if (layoutResult.metadata) {
5414
+ layoutMetadata.push(layoutResult.metadata);
5415
+ }
4305
5416
  } catch (error) {
4306
- reqLogger.warn(`Layout server hook ${i} failed`, {
4307
- error,
4308
- layoutFile: route.layoutFiles[i],
4309
- route: route.pattern
5417
+ const layoutFile = route.layoutFiles[i];
5418
+ const relativeLayoutPath = layoutFile ? path21.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5419
+ reqLogger.warn("Layout server hook failed", {
5420
+ error: error instanceof Error ? error.message : String(error),
5421
+ stack: error instanceof Error ? error.stack : void 0,
5422
+ layoutFile: relativeLayoutPath,
5423
+ route: route.pattern,
5424
+ layoutIndex: i,
5425
+ suggestion: "Check your layout.server.hook.ts file for errors"
4310
5426
  });
4311
5427
  }
4312
5428
  }
@@ -4319,10 +5435,25 @@ async function handlePageRequestInternal(options) {
4319
5435
  loaderResult.theme = theme;
4320
5436
  }
4321
5437
  } catch (error) {
5438
+ const relativePagePath = route.pageFile ? path21.relative(projectRoot || process.cwd(), route.pageFile) : "unknown";
5439
+ reqLogger.error("Page server hook failed", {
5440
+ error: error instanceof Error ? error.message : String(error),
5441
+ stack: error instanceof Error ? error.stack : void 0,
5442
+ pageFile: relativePagePath,
5443
+ route: route.pattern,
5444
+ pathname: urlPath,
5445
+ suggestion: "Check your page.server.hook.ts (or server.hook.ts) file for errors"
5446
+ });
4322
5447
  if (isDataReq) {
4323
5448
  res.statusCode = 500;
4324
5449
  res.setHeader("Content-Type", "application/json; charset=utf-8");
4325
- res.end(JSON.stringify({ error: true, message: String(error) }));
5450
+ const errorResponse = {
5451
+ error: true,
5452
+ message: error instanceof Error ? error.message : String(error),
5453
+ route: route.pattern,
5454
+ pageFile: relativePagePath
5455
+ };
5456
+ res.end(JSON.stringify(errorResponse, null, 2));
4326
5457
  return;
4327
5458
  } else {
4328
5459
  if (errorPage) {
@@ -4339,9 +5470,19 @@ async function handlePageRequestInternal(options) {
4339
5470
  ...loaderResult.props || {}
4340
5471
  // Props from page (overrides layout)
4341
5472
  };
5473
+ let combinedMetadata = null;
5474
+ for (const layoutMeta of layoutMetadata) {
5475
+ if (layoutMeta) {
5476
+ combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
5477
+ }
5478
+ }
5479
+ if (loaderResult.metadata) {
5480
+ combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
5481
+ }
4342
5482
  const combinedLoaderResult = {
4343
5483
  ...loaderResult,
4344
- props: combinedProps
5484
+ props: combinedProps,
5485
+ metadata: combinedMetadata
4345
5486
  };
4346
5487
  if (isDataReq) {
4347
5488
  handleDataResponse(res, combinedLoaderResult, theme);
@@ -4371,6 +5512,10 @@ async function handlePageRequestInternal(options) {
4371
5512
  }
4372
5513
  }
4373
5514
  const nonce = res.locals.nonce || void 0;
5515
+ const entrypointFiles = [];
5516
+ if (assetManifest?.entrypoints?.client) {
5517
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5518
+ }
4374
5519
  const documentTree = createDocumentTree({
4375
5520
  appTree,
4376
5521
  initialData,
@@ -4379,6 +5524,7 @@ async function handlePageRequestInternal(options) {
4379
5524
  titleFallback: "Loly framework",
4380
5525
  descriptionFallback: "Loly demo",
4381
5526
  chunkHref,
5527
+ entrypointFiles,
4382
5528
  theme,
4383
5529
  clientJsPath,
4384
5530
  clientCssPath,
@@ -4397,7 +5543,16 @@ async function handlePageRequestInternal(options) {
4397
5543
  onShellError(err) {
4398
5544
  didError = true;
4399
5545
  const reqLogger2 = getRequestLogger(req);
4400
- reqLogger2.error("SSR shell error", err, { route: matched?.route?.pattern || "unknown" });
5546
+ const routePattern = matched?.route?.pattern || "unknown";
5547
+ reqLogger2.error("SSR shell error", err, { route: routePattern });
5548
+ const errorMessage = err instanceof Error ? err.message : String(err);
5549
+ console.error(`
5550
+ \u274C [framework][ssr] Shell error for route "${routePattern}":`);
5551
+ console.error(` ${errorMessage}`);
5552
+ if (err instanceof Error && err.stack) {
5553
+ console.error(` Stack: ${err.stack.split("\n").slice(0, 3).join("\n ")}`);
5554
+ }
5555
+ console.error("\u{1F4A1} This usually indicates a React rendering error\n");
4401
5556
  if (!res.headersSent && errorPage) {
4402
5557
  renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
4403
5558
  } else if (!res.headersSent) {
@@ -4410,7 +5565,11 @@ async function handlePageRequestInternal(options) {
4410
5565
  onError(err) {
4411
5566
  didError = true;
4412
5567
  const reqLogger2 = getRequestLogger(req);
4413
- reqLogger2.error("SSR error", err, { route: matched?.route?.pattern || "unknown" });
5568
+ const routePattern = matched?.route?.pattern || "unknown";
5569
+ reqLogger2.error("SSR error", err, { route: routePattern });
5570
+ const errorMessage = err instanceof Error ? err.message : String(err);
5571
+ console.error(`\u26A0\uFE0F [framework][ssr] Error during streaming for route "${routePattern}":`);
5572
+ console.error(` ${errorMessage}`);
4414
5573
  }
4415
5574
  });
4416
5575
  req.on("close", () => {
@@ -4439,9 +5598,13 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4439
5598
  Object.assign(layoutProps, layoutResult.props);
4440
5599
  }
4441
5600
  } catch (err) {
4442
- reqLogger.warn(`Layout server hook ${i} failed for error page`, {
4443
- error: err,
4444
- layoutFile: errorPage.layoutFiles[i]
5601
+ const layoutFile = errorPage.layoutFiles[i];
5602
+ const relativeLayoutPath = layoutFile ? path21.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5603
+ reqLogger.warn("Layout server hook failed for error page", {
5604
+ error: err instanceof Error ? err.message : String(err),
5605
+ stack: err instanceof Error ? err.stack : void 0,
5606
+ layoutFile: relativeLayoutPath,
5607
+ layoutIndex: i
4445
5608
  });
4446
5609
  }
4447
5610
  }
@@ -4488,6 +5651,10 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4488
5651
  }
4489
5652
  }
4490
5653
  const nonce = res.locals.nonce || void 0;
5654
+ const entrypointFiles = [];
5655
+ if (assetManifest?.entrypoints?.client) {
5656
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5657
+ }
4491
5658
  const documentTree = createDocumentTree({
4492
5659
  appTree,
4493
5660
  initialData,
@@ -4613,7 +5780,7 @@ async function getServerConfig(projectRoot) {
4613
5780
  }
4614
5781
 
4615
5782
  // modules/server/routes.ts
4616
- import path19 from "path";
5783
+ import path22 from "path";
4617
5784
  function setupRoutes(options) {
4618
5785
  const {
4619
5786
  app,
@@ -4628,21 +5795,23 @@ function setupRoutes(options) {
4628
5795
  config
4629
5796
  } = options;
4630
5797
  const routeChunks = routeLoader.loadRouteChunks();
4631
- const ssgOutDir = path19.join(
4632
- config ? getBuildDir(projectRoot, config) : path19.join(projectRoot, BUILD_FOLDER_NAME),
5798
+ const ssgOutDir = path22.join(
5799
+ config ? getBuildDir(projectRoot, config) : path22.join(projectRoot, BUILD_FOLDER_NAME),
4633
5800
  "ssg"
4634
5801
  );
4635
5802
  app.all("/api/*", async (req, res) => {
4636
5803
  const apiRoutes = isDev && getRoutes ? getRoutes().apiRoutes : initialApiRoutes;
4637
5804
  const serverConfig = await getServerConfig(projectRoot);
4638
5805
  const strictPatterns = serverConfig.rateLimit?.strictPatterns || [];
5806
+ const rateLimitConfig = serverConfig.rateLimit;
4639
5807
  await handleApiRequest({
4640
5808
  apiRoutes,
4641
5809
  urlPath: req.path,
4642
5810
  req,
4643
5811
  res,
4644
5812
  env: isDev ? "dev" : "prod",
4645
- strictRateLimitPatterns: strictPatterns
5813
+ strictRateLimitPatterns: strictPatterns,
5814
+ rateLimitConfig
4646
5815
  });
4647
5816
  });
4648
5817
  app.get("*", async (req, res) => {
@@ -4895,12 +6064,29 @@ var setupApplication = async ({
4895
6064
  corsOptions.origin = process.env.NODE_ENV === "development";
4896
6065
  }
4897
6066
  app.use(cors(corsOptions));
4898
- if (rateLimit2 && process.env.NODE_ENV !== "development") {
4899
- const generalLimiter = createRateLimiter({
4900
- windowMs: rateLimit2.windowMs,
4901
- max: rateLimit2.max
4902
- });
4903
- app.use(generalLimiter);
6067
+ if (rateLimit2) {
6068
+ const shouldApply = process.env.NODE_ENV !== "development" || process.env.ENABLE_RATE_LIMIT === "true";
6069
+ if (shouldApply) {
6070
+ try {
6071
+ const generalLimiter = createRateLimiterFromConfig(rateLimit2, false);
6072
+ if (generalLimiter) {
6073
+ app.use(generalLimiter);
6074
+ const logger5 = createModuleLogger("server");
6075
+ logger5.info("Rate limiting enabled", {
6076
+ windowMs: rateLimit2.windowMs ?? 15 * 60 * 1e3,
6077
+ max: rateLimit2.max ?? 100,
6078
+ apiMax: rateLimit2.apiMax,
6079
+ strictMax: rateLimit2.strictMax,
6080
+ strictPatterns: rateLimit2.strictPatterns?.length ?? 0
6081
+ });
6082
+ }
6083
+ } catch (error) {
6084
+ const logger5 = createModuleLogger("server");
6085
+ logger5.error("Failed to setup rate limiting", {
6086
+ error: error instanceof Error ? error.message : String(error)
6087
+ });
6088
+ }
6089
+ }
4904
6090
  }
4905
6091
  app.use(cookieParser());
4906
6092
  app.use(express2.json({ limit: bodyLimit }));
@@ -4915,22 +6101,31 @@ var setupApplication = async ({
4915
6101
 
4916
6102
  // src/server.ts
4917
6103
  import dotenv2 from "dotenv";
4918
- var envPath = path20.join(process.cwd(), ".env");
6104
+ var envPath = path23.join(process.cwd(), ".env");
4919
6105
  if (fs16.existsSync(envPath)) {
4920
6106
  dotenv2.config({ path: envPath });
4921
6107
  } else {
4922
6108
  dotenv2.config();
4923
6109
  }
4924
- var logger3 = createModuleLogger("server");
6110
+ var logger4 = createModuleLogger("server");
4925
6111
  async function startServer(options = {}) {
4926
6112
  const isDev = options.isDev ?? process.env.NODE_ENV === "development";
4927
6113
  const projectRoot = options.rootDir ?? process.cwd();
4928
- const config = options.config ?? loadConfig(projectRoot);
6114
+ let config;
6115
+ try {
6116
+ config = options.config ?? loadConfig(projectRoot);
6117
+ } catch (error) {
6118
+ if (error instanceof ConfigValidationError) {
6119
+ console.error("\n" + error.message + "\n");
6120
+ process.exit(1);
6121
+ }
6122
+ throw error;
6123
+ }
4929
6124
  const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? config.server.port;
4930
6125
  const host = process.env.HOST ?? (!isDev ? "0.0.0.0" : void 0) ?? config.server.host;
4931
- const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : path20.join(getBuildDir(projectRoot, config), "server"));
6126
+ const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : path23.join(getBuildDir(projectRoot, config), "server"));
4932
6127
  if (!isDev && !fs16.existsSync(appDir)) {
4933
- logger3.error("Compiled directory not found", void 0, {
6128
+ logger4.error("Compiled directory not found", void 0, {
4934
6129
  buildDir: config.directories.build,
4935
6130
  appDir,
4936
6131
  environment: "production"
@@ -4947,7 +6142,7 @@ async function startServer(options = {}) {
4947
6142
  isDev,
4948
6143
  config
4949
6144
  });
4950
- const routeLoader = isDev ? new FilesystemRouteLoader(appDir) : new ManifestRouteLoader(projectRoot);
6145
+ const routeLoader = isDev ? new FilesystemRouteLoader(appDir, projectRoot) : new ManifestRouteLoader(projectRoot);
4951
6146
  setupWssEvents({
4952
6147
  httpServer,
4953
6148
  wssRoutes
@@ -4999,10 +6194,10 @@ async function startProdServer(options = {}) {
4999
6194
  }
5000
6195
 
5001
6196
  // modules/build/ssg/builder.ts
5002
- import path23 from "path";
6197
+ import path26 from "path";
5003
6198
 
5004
6199
  // modules/build/ssg/path.ts
5005
- import path21 from "path";
6200
+ import path24 from "path";
5006
6201
  function buildPathFromPattern(pattern, params) {
5007
6202
  const segments = pattern.split("/").filter(Boolean);
5008
6203
  const parts = [];
@@ -5031,12 +6226,12 @@ function buildPathFromPattern(pattern, params) {
5031
6226
  }
5032
6227
  function pathToOutDir(baseDir, urlPath) {
5033
6228
  const clean = urlPath === "/" ? "" : urlPath.replace(/^\/+/, "");
5034
- return path21.join(baseDir, clean);
6229
+ return path24.join(baseDir, clean);
5035
6230
  }
5036
6231
 
5037
6232
  // modules/build/ssg/renderer.ts
5038
6233
  import fs17 from "fs";
5039
- import path22 from "path";
6234
+ import path25 from "path";
5040
6235
  import { renderToString } from "react-dom/server";
5041
6236
  init_globals();
5042
6237
  async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params) {
@@ -5053,6 +6248,10 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
5053
6248
  chunkHref = `${STATIC_PATH}/${chunkName}.js`;
5054
6249
  }
5055
6250
  }
6251
+ const entrypointFiles = [];
6252
+ if (assetManifest?.entrypoints?.client) {
6253
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
6254
+ }
5056
6255
  const req = {
5057
6256
  method: "GET",
5058
6257
  headers: {},
@@ -5082,32 +6281,79 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
5082
6281
  })
5083
6282
  );
5084
6283
  }
6284
+ const layoutProps = {};
6285
+ const layoutMetadata = [];
6286
+ if (route.layoutServerHooks && route.layoutServerHooks.length > 0) {
6287
+ for (let i = 0; i < route.layoutServerHooks.length; i++) {
6288
+ const layoutServerHook = route.layoutServerHooks[i];
6289
+ if (layoutServerHook) {
6290
+ try {
6291
+ const layoutResult = await layoutServerHook(ctx);
6292
+ if (layoutResult.props) {
6293
+ Object.assign(layoutProps, layoutResult.props);
6294
+ }
6295
+ if (layoutResult.metadata) {
6296
+ layoutMetadata.push(layoutResult.metadata);
6297
+ }
6298
+ } catch (error) {
6299
+ console.warn(
6300
+ `\u26A0\uFE0F [framework][ssg] Layout server hook ${i} failed for route ${route.pattern}:`,
6301
+ error instanceof Error ? error.message : String(error)
6302
+ );
6303
+ if (error instanceof Error && error.stack) {
6304
+ console.warn(` Stack: ${error.stack.split("\n").slice(0, 3).join("\n ")}`);
6305
+ }
6306
+ }
6307
+ }
6308
+ }
6309
+ }
5085
6310
  let loaderResult = { props: {} };
5086
6311
  if (route.loader) {
5087
6312
  loaderResult = await route.loader(ctx);
5088
6313
  }
5089
- if (loaderResult.redirect || loaderResult.notFound) {
6314
+ const combinedProps = {
6315
+ ...layoutProps,
6316
+ ...loaderResult.props || {}
6317
+ };
6318
+ let combinedMetadata = null;
6319
+ for (const layoutMeta of layoutMetadata) {
6320
+ if (layoutMeta) {
6321
+ combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
6322
+ }
6323
+ }
6324
+ if (loaderResult.metadata) {
6325
+ combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
6326
+ }
6327
+ const combinedLoaderResult = {
6328
+ ...loaderResult,
6329
+ props: combinedProps,
6330
+ metadata: combinedMetadata
6331
+ };
6332
+ if (combinedLoaderResult.redirect || combinedLoaderResult.notFound) {
5090
6333
  return;
5091
6334
  }
5092
- const initialData = buildInitialData(urlPath, params, loaderResult);
6335
+ const initialData = buildInitialData(urlPath, params, combinedLoaderResult);
5093
6336
  const routerData = buildRouterData(req);
5094
6337
  const appTree = buildAppTree(route, params, initialData.props);
5095
6338
  const documentTree = createDocumentTree({
5096
6339
  appTree,
5097
6340
  initialData,
5098
6341
  routerData,
5099
- meta: loaderResult.metadata,
6342
+ meta: combinedLoaderResult.metadata,
5100
6343
  titleFallback: "My Framework Dev",
5101
6344
  descriptionFallback: "Static page generated by @lolyjs/core.",
5102
6345
  chunkHref,
6346
+ entrypointFiles,
5103
6347
  clientJsPath,
5104
- clientCssPath
6348
+ clientCssPath,
6349
+ includeInlineScripts: true
6350
+ // SSG needs inline scripts (renderToString doesn't support bootstrapScripts)
5105
6351
  });
5106
6352
  const html = "<!DOCTYPE html>" + renderToString(documentTree);
5107
6353
  const dir = pathToOutDir(ssgOutDir, urlPath);
5108
6354
  ensureDir(dir);
5109
- const htmlFile = path22.join(dir, "index.html");
5110
- const dataFile = path22.join(dir, "data.json");
6355
+ const htmlFile = path25.join(dir, "index.html");
6356
+ const dataFile = path25.join(dir, "data.json");
5111
6357
  fs17.writeFileSync(htmlFile, html, "utf-8");
5112
6358
  fs17.writeFileSync(dataFile, JSON.stringify(initialData, null, 2), "utf-8");
5113
6359
  }
@@ -5115,7 +6361,7 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
5115
6361
  // modules/build/ssg/builder.ts
5116
6362
  init_globals();
5117
6363
  async function buildStaticPages(projectRoot, routes) {
5118
- const ssgOutDir = path23.join(projectRoot, BUILD_FOLDER_NAME, "ssg");
6364
+ const ssgOutDir = path26.join(projectRoot, BUILD_FOLDER_NAME, "ssg");
5119
6365
  ensureDir(ssgOutDir);
5120
6366
  for (const route of routes) {
5121
6367
  if (route.dynamic !== "force-static") continue;
@@ -5125,12 +6371,15 @@ async function buildStaticPages(projectRoot, routes) {
5125
6371
  } else {
5126
6372
  if (!route.generateStaticParams) {
5127
6373
  console.warn(
5128
- `[framework][ssg] Route ${route.pattern} is marked as force-static but has no generateStaticParams function. Skipping.`
6374
+ `\u26A0\uFE0F [framework][ssg] Route "${route.pattern}" is marked as force-static but has no generateStaticParams function`
5129
6375
  );
6376
+ console.warn(` \u{1F4A1} Add a generateStaticParams export to enable static generation for this route`);
6377
+ console.warn(` Skipping this route...
6378
+ `);
5130
6379
  continue;
5131
6380
  }
5132
6381
  try {
5133
- console.log(`[framework][ssg] Generating static params for route: ${route.pattern}`);
6382
+ console.log(`\u{1F4E6} [framework][ssg] Generating static params for route: ${route.pattern}`);
5134
6383
  let timeoutId = null;
5135
6384
  const timeoutPromise = new Promise((_, reject) => {
5136
6385
  timeoutId = setTimeout(() => {
@@ -5145,12 +6394,16 @@ async function buildStaticPages(projectRoot, routes) {
5145
6394
  clearTimeout(timeoutId);
5146
6395
  }
5147
6396
  allParams = sp;
5148
- console.log(`[framework][ssg] Generated ${sp.length} static params for route: ${route.pattern}`);
6397
+ console.log(` \u2705 Generated ${sp.length} static params for route: ${route.pattern}`);
5149
6398
  } catch (error) {
5150
- console.error(
5151
- `[framework][ssg] Error generating static params for route ${route.pattern}:`,
5152
- error
5153
- );
6399
+ console.error(`
6400
+ \u274C [framework][ssg] Error generating static params for route "${route.pattern}":`);
6401
+ console.error(error instanceof Error ? error.message : String(error));
6402
+ if (error instanceof Error && error.stack) {
6403
+ console.error(` Stack: ${error.stack.split("\n").slice(0, 3).join("\n ")}`);
6404
+ }
6405
+ console.error(`\u{1F4A1} Check your generateStaticParams function for this route
6406
+ `);
5154
6407
  throw error;
5155
6408
  }
5156
6409
  }
@@ -5159,11 +6412,11 @@ async function buildStaticPages(projectRoot, routes) {
5159
6412
  await renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params);
5160
6413
  }
5161
6414
  }
5162
- console.log(`[framework][ssg] Finished building all static pages`);
6415
+ console.log(`\u2705 [framework][ssg] Finished building all static pages`);
5163
6416
  }
5164
6417
 
5165
6418
  // modules/build/bundler/server.ts
5166
- import path24 from "path";
6419
+ import path27 from "path";
5167
6420
  import fs18 from "fs";
5168
6421
  import esbuild from "esbuild";
5169
6422
  init_globals();
@@ -5173,7 +6426,7 @@ function collectAppSources(appDir) {
5173
6426
  function walk(dir) {
5174
6427
  const items = fs18.readdirSync(dir, { withFileTypes: true });
5175
6428
  for (const item of items) {
5176
- const full = path24.join(dir, item.name);
6429
+ const full = path27.join(dir, item.name);
5177
6430
  if (item.isDirectory()) {
5178
6431
  walk(full);
5179
6432
  continue;
@@ -5190,7 +6443,7 @@ function collectAppSources(appDir) {
5190
6443
  return entries;
5191
6444
  }
5192
6445
  async function buildServerApp(projectRoot, appDir) {
5193
- const outDir = path24.join(projectRoot, BUILD_FOLDER_NAME, "server");
6446
+ const outDir = path27.join(projectRoot, BUILD_FOLDER_NAME, "server");
5194
6447
  const entryPoints = collectAppSources(appDir);
5195
6448
  ensureDir(outDir);
5196
6449
  if (entryPoints.length === 0) {
@@ -5208,12 +6461,12 @@ async function buildServerApp(projectRoot, appDir) {
5208
6461
  bundle: true,
5209
6462
  splitting: false,
5210
6463
  logLevel: "info",
5211
- tsconfig: path24.join(projectRoot, "tsconfig.json"),
6464
+ tsconfig: path27.join(projectRoot, "tsconfig.json"),
5212
6465
  packages: "external"
5213
6466
  });
5214
6467
  for (const fileName of SERVER_FILES) {
5215
- const initTS = path24.join(projectRoot, `${fileName}.ts`);
5216
- const initJS = path24.join(outDir, `${fileName}.js`);
6468
+ const initTS = path27.join(projectRoot, `${fileName}.ts`);
6469
+ const initJS = path27.join(outDir, `${fileName}.js`);
5217
6470
  if (fs18.existsSync(initTS)) {
5218
6471
  await esbuild.build({
5219
6472
  entryPoints: [initTS],
@@ -5225,7 +6478,7 @@ async function buildServerApp(projectRoot, appDir) {
5225
6478
  sourcemap: true,
5226
6479
  bundle: false,
5227
6480
  logLevel: "info",
5228
- tsconfig: path24.join(projectRoot, "tsconfig.json")
6481
+ tsconfig: path27.join(projectRoot, "tsconfig.json")
5229
6482
  });
5230
6483
  }
5231
6484
  }
@@ -5368,22 +6621,158 @@ function matchRouteClient(pathWithSearch, routes) {
5368
6621
  }
5369
6622
 
5370
6623
  // modules/runtime/client/metadata.ts
6624
+ function getOrCreateMeta(selector, attributes) {
6625
+ let meta = document.querySelector(selector);
6626
+ if (!meta) {
6627
+ meta = document.createElement("meta");
6628
+ if (attributes.name) meta.name = attributes.name;
6629
+ if (attributes.property) meta.setAttribute("property", attributes.property);
6630
+ if (attributes.httpEquiv) meta.httpEquiv = attributes.httpEquiv;
6631
+ document.head.appendChild(meta);
6632
+ }
6633
+ return meta;
6634
+ }
6635
+ function getOrCreateLink(rel, href) {
6636
+ const selector = `link[rel="${rel}"]`;
6637
+ let link = document.querySelector(selector);
6638
+ if (!link) {
6639
+ link = document.createElement("link");
6640
+ link.rel = rel;
6641
+ link.href = href;
6642
+ document.head.appendChild(link);
6643
+ } else {
6644
+ link.href = href;
6645
+ }
6646
+ return link;
6647
+ }
5371
6648
  function applyMetadata(md) {
5372
6649
  if (!md) return;
5373
6650
  if (md.title) {
5374
6651
  document.title = md.title;
5375
6652
  }
5376
6653
  if (md.description) {
5377
- let meta = document.querySelector(
5378
- 'meta[name="description"]'
5379
- );
5380
- if (!meta) {
5381
- meta = document.createElement("meta");
5382
- meta.name = "description";
5383
- document.head.appendChild(meta);
5384
- }
6654
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
5385
6655
  meta.content = md.description;
5386
6656
  }
6657
+ if (md.robots) {
6658
+ const meta = getOrCreateMeta('meta[name="robots"]', { name: "robots" });
6659
+ meta.content = md.robots;
6660
+ }
6661
+ if (md.themeColor) {
6662
+ const meta = getOrCreateMeta('meta[name="theme-color"]', { name: "theme-color" });
6663
+ meta.content = md.themeColor;
6664
+ }
6665
+ if (md.viewport) {
6666
+ const meta = getOrCreateMeta('meta[name="viewport"]', { name: "viewport" });
6667
+ meta.content = md.viewport;
6668
+ }
6669
+ if (md.canonical) {
6670
+ getOrCreateLink("canonical", md.canonical);
6671
+ }
6672
+ if (md.openGraph) {
6673
+ const og = md.openGraph;
6674
+ if (og.title) {
6675
+ const meta = getOrCreateMeta('meta[property="og:title"]', { property: "og:title" });
6676
+ meta.content = og.title;
6677
+ }
6678
+ if (og.description) {
6679
+ const meta = getOrCreateMeta('meta[property="og:description"]', { property: "og:description" });
6680
+ meta.content = og.description;
6681
+ }
6682
+ if (og.type) {
6683
+ const meta = getOrCreateMeta('meta[property="og:type"]', { property: "og:type" });
6684
+ meta.content = og.type;
6685
+ }
6686
+ if (og.url) {
6687
+ const meta = getOrCreateMeta('meta[property="og:url"]', { property: "og:url" });
6688
+ meta.content = og.url;
6689
+ }
6690
+ if (og.image) {
6691
+ if (typeof og.image === "string") {
6692
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
6693
+ meta.content = og.image;
6694
+ } else {
6695
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
6696
+ meta.content = og.image.url;
6697
+ if (og.image.width) {
6698
+ const metaWidth = getOrCreateMeta('meta[property="og:image:width"]', { property: "og:image:width" });
6699
+ metaWidth.content = String(og.image.width);
6700
+ }
6701
+ if (og.image.height) {
6702
+ const metaHeight = getOrCreateMeta('meta[property="og:image:height"]', { property: "og:image:height" });
6703
+ metaHeight.content = String(og.image.height);
6704
+ }
6705
+ if (og.image.alt) {
6706
+ const metaAlt = getOrCreateMeta('meta[property="og:image:alt"]', { property: "og:image:alt" });
6707
+ metaAlt.content = og.image.alt;
6708
+ }
6709
+ }
6710
+ }
6711
+ if (og.siteName) {
6712
+ const meta = getOrCreateMeta('meta[property="og:site_name"]', { property: "og:site_name" });
6713
+ meta.content = og.siteName;
6714
+ }
6715
+ if (og.locale) {
6716
+ const meta = getOrCreateMeta('meta[property="og:locale"]', { property: "og:locale" });
6717
+ meta.content = og.locale;
6718
+ }
6719
+ }
6720
+ if (md.twitter) {
6721
+ const twitter = md.twitter;
6722
+ if (twitter.card) {
6723
+ const meta = getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" });
6724
+ meta.content = twitter.card;
6725
+ }
6726
+ if (twitter.title) {
6727
+ const meta = getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" });
6728
+ meta.content = twitter.title;
6729
+ }
6730
+ if (twitter.description) {
6731
+ const meta = getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" });
6732
+ meta.content = twitter.description;
6733
+ }
6734
+ if (twitter.image) {
6735
+ const meta = getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" });
6736
+ meta.content = twitter.image;
6737
+ }
6738
+ if (twitter.imageAlt) {
6739
+ const meta = getOrCreateMeta('meta[name="twitter:image:alt"]', { name: "twitter:image:alt" });
6740
+ meta.content = twitter.imageAlt;
6741
+ }
6742
+ if (twitter.site) {
6743
+ const meta = getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" });
6744
+ meta.content = twitter.site;
6745
+ }
6746
+ if (twitter.creator) {
6747
+ const meta = getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" });
6748
+ meta.content = twitter.creator;
6749
+ }
6750
+ }
6751
+ if (md.metaTags && Array.isArray(md.metaTags)) {
6752
+ md.metaTags.forEach((tag) => {
6753
+ let selector = "";
6754
+ if (tag.name) {
6755
+ selector = `meta[name="${tag.name}"]`;
6756
+ } else if (tag.property) {
6757
+ selector = `meta[property="${tag.property}"]`;
6758
+ } else if (tag.httpEquiv) {
6759
+ selector = `meta[http-equiv="${tag.httpEquiv}"]`;
6760
+ }
6761
+ if (selector) {
6762
+ const meta = getOrCreateMeta(selector, {
6763
+ name: tag.name,
6764
+ property: tag.property,
6765
+ httpEquiv: tag.httpEquiv
6766
+ });
6767
+ meta.content = tag.content;
6768
+ }
6769
+ });
6770
+ }
6771
+ if (md.links && Array.isArray(md.links)) {
6772
+ md.links.forEach((link) => {
6773
+ getOrCreateLink(link.rel, link.href);
6774
+ });
6775
+ }
5387
6776
  }
5388
6777
 
5389
6778
  // modules/runtime/client/AppShell.tsx
@@ -5600,10 +6989,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
5600
6989
  });
5601
6990
  return true;
5602
6991
  } catch (loadError) {
5603
- console.error(
5604
- "[client] Error loading error route components:",
5605
- loadError
5606
- );
6992
+ console.error("\n\u274C [client] Error loading error route components:");
6993
+ console.error(loadError);
6994
+ if (loadError instanceof Error) {
6995
+ console.error(` Message: ${loadError.message}`);
6996
+ if (loadError.stack) {
6997
+ console.error(` Stack: ${loadError.stack.split("\n").slice(0, 3).join("\n ")}`);
6998
+ }
6999
+ }
7000
+ console.error("\u{1F4A1} Falling back to full page reload\n");
5607
7001
  window.location.href = nextUrl;
5608
7002
  return false;
5609
7003
  }
@@ -5708,7 +7102,11 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
5708
7102
  searchParams: Object.fromEntries(url.searchParams.entries())
5709
7103
  };
5710
7104
  setRouterData(routerData);
5711
- const components = await matched.route.load();
7105
+ const prefetched = prefetchedRoutes.get(matched.route);
7106
+ const components = prefetched ? await prefetched : await matched.route.load();
7107
+ if (!prefetched) {
7108
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
7109
+ }
5712
7110
  window.scrollTo({
5713
7111
  top: 0,
5714
7112
  behavior: "smooth"
@@ -5747,7 +7145,7 @@ async function navigate(nextUrl, handlers, options) {
5747
7145
  }
5748
7146
  }
5749
7147
  if (!ok) {
5750
- if (json && json.redirect) {
7148
+ if (json?.redirect) {
5751
7149
  window.location.href = json.redirect.destination;
5752
7150
  return;
5753
7151
  }
@@ -5768,6 +7166,47 @@ async function navigate(nextUrl, handlers, options) {
5768
7166
  window.location.href = nextUrl;
5769
7167
  }
5770
7168
  }
7169
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
7170
+ function prefetchRoute(url, routes, notFoundRoute) {
7171
+ const [pathname] = url.split("?");
7172
+ const matched = matchRouteClient(pathname, routes);
7173
+ if (!matched) {
7174
+ if (notFoundRoute) {
7175
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
7176
+ if (!existing2) {
7177
+ const promise = notFoundRoute.load();
7178
+ prefetchedRoutes.set(notFoundRoute, promise);
7179
+ }
7180
+ }
7181
+ return;
7182
+ }
7183
+ const existing = prefetchedRoutes.get(matched.route);
7184
+ if (!existing) {
7185
+ const promise = matched.route.load();
7186
+ prefetchedRoutes.set(matched.route, promise);
7187
+ }
7188
+ }
7189
+ function createHoverHandler(routes, notFoundRoute) {
7190
+ return function handleHover(ev) {
7191
+ try {
7192
+ const target = ev.target;
7193
+ if (!target) return;
7194
+ const anchor = target.closest("a[href]");
7195
+ if (!anchor) return;
7196
+ const href = anchor.getAttribute("href");
7197
+ if (!href) return;
7198
+ if (href.startsWith("#")) return;
7199
+ const url = new URL(href, window.location.href);
7200
+ if (url.origin !== window.location.origin) return;
7201
+ if (anchor.target && anchor.target !== "_self") return;
7202
+ const nextUrl = url.pathname + url.search;
7203
+ const currentUrl = window.location.pathname + window.location.search;
7204
+ if (nextUrl === currentUrl) return;
7205
+ prefetchRoute(nextUrl, routes, notFoundRoute);
7206
+ } catch (error) {
7207
+ }
7208
+ };
7209
+ }
5771
7210
  function createClickHandler(navigate2) {
5772
7211
  return function handleClick(ev) {
5773
7212
  try {
@@ -5878,17 +7317,20 @@ function AppShell({
5878
7317
  }
5879
7318
  const handleClick = createClickHandler(handleNavigateInternal);
5880
7319
  const handlePopState = createPopStateHandler(handleNavigateInternal);
7320
+ const handleHover = createHoverHandler(routes, notFoundRoute);
5881
7321
  window.addEventListener("click", handleClick, false);
5882
7322
  window.addEventListener("popstate", handlePopState, false);
7323
+ window.addEventListener("mouseover", handleHover, false);
5883
7324
  return () => {
5884
7325
  isMounted = false;
5885
7326
  window.removeEventListener("click", handleClick, false);
5886
7327
  window.removeEventListener("popstate", handlePopState, false);
7328
+ window.removeEventListener("mouseover", handleHover, false);
5887
7329
  };
5888
- }, []);
7330
+ }, [routes, notFoundRoute]);
5889
7331
  useEffect(() => {
5890
7332
  const handleDataRefresh = () => {
5891
- const freshData = window?.__FW_DATA__;
7333
+ const freshData = window[WINDOW_DATA_KEY2];
5892
7334
  if (!freshData) return;
5893
7335
  const currentPathname = window.location.pathname;
5894
7336
  const freshPathname = freshData.pathname;
@@ -5915,6 +7357,91 @@ function AppShell({
5915
7357
  return /* @__PURE__ */ jsx2(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ jsx2(RouterView, { state }, routeKey) });
5916
7358
  }
5917
7359
 
7360
+ // modules/runtime/client/hot-reload.ts
7361
+ function setupHotReload2() {
7362
+ const nodeEnv = process.env.NODE_ENV || "production";
7363
+ const isDev = nodeEnv === "development";
7364
+ console.log(`[hot-reload] NODE_ENV: ${nodeEnv}, isDev: ${isDev}`);
7365
+ if (!isDev) {
7366
+ console.log("[hot-reload] Skipping hot reload setup (not in development mode)");
7367
+ return;
7368
+ }
7369
+ console.log("[hot-reload] Setting up hot reload client...");
7370
+ let eventSource = null;
7371
+ let reloadTimeout = null;
7372
+ let reconnectTimeout = null;
7373
+ let reconnectAttempts = 0;
7374
+ const MAX_RECONNECT_ATTEMPTS = 10;
7375
+ const RECONNECT_DELAY = 1e3;
7376
+ const RELOAD_DELAY = 100;
7377
+ function connect() {
7378
+ try {
7379
+ if (eventSource) {
7380
+ console.log("[hot-reload] Closing existing EventSource connection");
7381
+ eventSource.close();
7382
+ }
7383
+ const endpoint = "/__fw/hot";
7384
+ eventSource = new EventSource(endpoint);
7385
+ eventSource.addEventListener("ping", (event) => {
7386
+ if ("data" in event) {
7387
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
7388
+ }
7389
+ reconnectAttempts = 0;
7390
+ });
7391
+ eventSource.addEventListener("message", (event) => {
7392
+ const data = event.data;
7393
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
7394
+ const filePath = data.slice(7);
7395
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
7396
+ if (reloadTimeout) {
7397
+ clearTimeout(reloadTimeout);
7398
+ }
7399
+ reloadTimeout = setTimeout(() => {
7400
+ try {
7401
+ window.location.reload();
7402
+ } catch (error) {
7403
+ console.error("[hot-reload] \u274C Error reloading page:", error);
7404
+ setTimeout(() => window.location.reload(), 100);
7405
+ }
7406
+ }, RELOAD_DELAY);
7407
+ }
7408
+ });
7409
+ eventSource.onopen = () => {
7410
+ reconnectAttempts = 0;
7411
+ };
7412
+ eventSource.onerror = (error) => {
7413
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
7414
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
7415
+ if (eventSource?.readyState === EventSource.CONNECTING) {
7416
+ console.log("[hot-reload] \u23F3 Still connecting...");
7417
+ return;
7418
+ } else if (eventSource?.readyState === EventSource.OPEN) {
7419
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
7420
+ } else {
7421
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
7422
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
7423
+ reconnectAttempts++;
7424
+ const delay = RECONNECT_DELAY * reconnectAttempts;
7425
+ if (reconnectTimeout) {
7426
+ clearTimeout(reconnectTimeout);
7427
+ }
7428
+ reconnectTimeout = setTimeout(() => {
7429
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
7430
+ connect();
7431
+ }, delay);
7432
+ } else {
7433
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
7434
+ }
7435
+ }
7436
+ };
7437
+ } catch (error) {
7438
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
7439
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
7440
+ }
7441
+ }
7442
+ connect();
7443
+ }
7444
+
5918
7445
  // modules/runtime/client/bootstrap.tsx
5919
7446
  import { jsx as jsx3 } from "react/jsx-runtime";
5920
7447
  async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute, errorRoute) {
@@ -5956,101 +7483,91 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
5956
7483
  props: initialData?.props ?? {}
5957
7484
  };
5958
7485
  }
5959
- function setupHotReload2() {
5960
- const nodeEnv = typeof process !== "undefined" && process.env?.NODE_ENV || "production";
5961
- const isDev = nodeEnv !== "production";
5962
- if (!isDev) {
5963
- return;
7486
+ function initializeRouterData(initialUrl, initialData) {
7487
+ let routerData = getRouterData();
7488
+ if (!routerData) {
7489
+ const url = new URL(initialUrl, window.location.origin);
7490
+ routerData = {
7491
+ pathname: url.pathname,
7492
+ params: initialData?.params || {},
7493
+ searchParams: Object.fromEntries(url.searchParams.entries())
7494
+ };
7495
+ setRouterData(routerData);
5964
7496
  }
7497
+ }
7498
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
5965
7499
  try {
5966
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
5967
- const eventSource = new EventSource("/__fw/hot");
5968
- let reloadTimeout = null;
5969
- eventSource.addEventListener("message", (event) => {
5970
- const data = event.data;
5971
- if (data && data.startsWith("reload:")) {
5972
- const filePath = data.slice(7);
5973
- console.log(`[hot-reload] File changed: ${filePath}`);
5974
- if (reloadTimeout) {
5975
- clearTimeout(reloadTimeout);
5976
- }
5977
- reloadTimeout = setTimeout(() => {
5978
- console.log("[hot-reload] Reloading page...");
5979
- window.location.reload();
5980
- }, 500);
5981
- }
5982
- });
5983
- eventSource.addEventListener("ping", () => {
5984
- console.log("[hot-reload] \u2713 Connected to hot reload server");
5985
- });
5986
- eventSource.onopen = () => {
5987
- console.log("[hot-reload] \u2713 SSE connection opened");
5988
- };
5989
- eventSource.onerror = (error) => {
5990
- const states = ["CONNECTING", "OPEN", "CLOSED"];
5991
- const state = states[eventSource.readyState] || "UNKNOWN";
5992
- if (eventSource.readyState === EventSource.CONNECTING) {
5993
- console.log("[hot-reload] Connecting...");
5994
- } else if (eventSource.readyState === EventSource.OPEN) {
5995
- console.warn("[hot-reload] Connection error (but connection is open):", error);
5996
- } else {
5997
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
7500
+ const initialState = await loadInitialRoute(
7501
+ initialUrl,
7502
+ initialData,
7503
+ routes,
7504
+ notFoundRoute,
7505
+ errorRoute
7506
+ );
7507
+ if (initialData?.metadata) {
7508
+ try {
7509
+ applyMetadata(initialData.metadata);
7510
+ } catch (metadataError) {
7511
+ console.warn("[client] Error applying metadata:", metadataError);
5998
7512
  }
5999
- };
7513
+ }
7514
+ hydrateRoot(
7515
+ container,
7516
+ /* @__PURE__ */ jsx3(
7517
+ AppShell,
7518
+ {
7519
+ initialState,
7520
+ routes,
7521
+ notFoundRoute,
7522
+ errorRoute
7523
+ }
7524
+ )
7525
+ );
6000
7526
  } catch (error) {
6001
- console.log("[hot-reload] EventSource not supported or error:", error);
7527
+ console.error(
7528
+ "[client] Error loading initial route components for",
7529
+ initialUrl,
7530
+ error
7531
+ );
7532
+ throw error;
6002
7533
  }
6003
7534
  }
6004
7535
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
6005
- console.log("[client] Bootstrap starting, setting up hot reload...");
6006
7536
  setupHotReload2();
6007
- (async function bootstrap() {
6008
- const container = document.getElementById(APP_CONTAINER_ID2);
6009
- const initialData = getWindowData();
6010
- if (!container) {
6011
- console.error(`Container #${APP_CONTAINER_ID2} not found for hydration`);
6012
- return;
6013
- }
6014
- const initialUrl = window.location.pathname + window.location.search;
6015
- let routerData = getRouterData();
6016
- if (!routerData) {
6017
- const url = new URL(initialUrl, window.location.origin);
6018
- routerData = {
6019
- pathname: url.pathname,
6020
- params: initialData?.params || {},
6021
- searchParams: Object.fromEntries(url.searchParams.entries())
6022
- };
6023
- setRouterData(routerData);
6024
- }
7537
+ (async () => {
6025
7538
  try {
6026
- const initialState = await loadInitialRoute(
7539
+ const container = document.getElementById(APP_CONTAINER_ID2);
7540
+ if (!container) {
7541
+ console.error(`
7542
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID2} not found`);
7543
+ console.error("\u{1F4A1} This usually means:");
7544
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
7545
+ console.error(" \u2022 The container was removed before hydration");
7546
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
7547
+ return;
7548
+ }
7549
+ const initialData = getWindowData();
7550
+ const initialUrl = window.location.pathname + window.location.search;
7551
+ initializeRouterData(initialUrl, initialData);
7552
+ await hydrateInitialRoute(
7553
+ container,
6027
7554
  initialUrl,
6028
7555
  initialData,
6029
7556
  routes,
6030
7557
  notFoundRoute,
6031
7558
  errorRoute
6032
7559
  );
6033
- if (initialData?.metadata) {
6034
- applyMetadata(initialData.metadata);
6035
- }
6036
- hydrateRoot(
6037
- container,
6038
- /* @__PURE__ */ jsx3(
6039
- AppShell,
6040
- {
6041
- initialState,
6042
- routes,
6043
- notFoundRoute,
6044
- errorRoute
6045
- }
6046
- )
6047
- );
6048
7560
  } catch (error) {
6049
- console.error(
6050
- "[client] Error loading initial route components for",
6051
- initialUrl,
6052
- error
6053
- );
7561
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
7562
+ console.error(error);
7563
+ if (error instanceof Error) {
7564
+ console.error("\nError details:");
7565
+ console.error(` Message: ${error.message}`);
7566
+ if (error.stack) {
7567
+ console.error(` Stack: ${error.stack}`);
7568
+ }
7569
+ }
7570
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
6054
7571
  window.location.reload();
6055
7572
  }
6056
7573
  })();
@@ -6078,11 +7595,11 @@ var ValidationError = class extends Error {
6078
7595
  format() {
6079
7596
  const formatted = {};
6080
7597
  for (const error of this.errors) {
6081
- const path25 = error.path.join(".");
6082
- if (!formatted[path25]) {
6083
- formatted[path25] = [];
7598
+ const path28 = error.path.join(".");
7599
+ if (!formatted[path28]) {
7600
+ formatted[path28] = [];
6084
7601
  }
6085
- formatted[path25].push(error.message);
7602
+ formatted[path28].push(error.message);
6086
7603
  }
6087
7604
  return formatted;
6088
7605
  }