@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.cjs CHANGED
@@ -116,7 +116,7 @@ module.exports = __toCommonJS(src_exports);
116
116
 
117
117
  // src/server.ts
118
118
  var import_fs18 = __toESM(require("fs"));
119
- var import_path22 = __toESM(require("path"));
119
+ var import_path25 = __toESM(require("path"));
120
120
 
121
121
  // modules/server/utils/server-dir.ts
122
122
  var import_fs = __toESM(require("fs"));
@@ -293,9 +293,26 @@ function loadServerHookForDir(currentDir) {
293
293
  generateStaticParams: null
294
294
  };
295
295
  }
296
- const middlewares = Array.isArray(
297
- mod?.[NAMING.BEFORE_MIDDLEWARES]
298
- ) ? mod[NAMING.BEFORE_MIDDLEWARES] : [];
296
+ let middlewares = [];
297
+ const rawMiddlewares = mod?.[NAMING.BEFORE_MIDDLEWARES];
298
+ if (rawMiddlewares !== void 0) {
299
+ if (!Array.isArray(rawMiddlewares)) {
300
+ console.warn(
301
+ `[framework][server-hook] ${NAMING.BEFORE_MIDDLEWARES} must be an array in ${file}, ignoring invalid value`
302
+ );
303
+ } else {
304
+ for (let i = 0; i < rawMiddlewares.length; i++) {
305
+ const mw = rawMiddlewares[i];
306
+ if (typeof mw !== "function") {
307
+ console.warn(
308
+ `[framework][server-hook] Middleware at index ${i} in ${NAMING.BEFORE_MIDDLEWARES} is not a function in ${file}, skipping`
309
+ );
310
+ continue;
311
+ }
312
+ middlewares.push(mw);
313
+ }
314
+ }
315
+ }
299
316
  const serverHook = typeof mod?.[NAMING.GET_SERVER_DATA_FN] === "function" ? mod[NAMING.GET_SERVER_DATA_FN] : null;
300
317
  const dynamic = mod?.[NAMING.RENDER_TYPE_CONST] === "force-static" || mod?.[NAMING.RENDER_TYPE_CONST] === "force-dynamic" ? mod.dynamic : "auto";
301
318
  const generateStaticParams = typeof mod?.[NAMING.GENERATE_SSG_PARAMS] === "function" ? mod[NAMING.GENERATE_SSG_PARAMS] : null;
@@ -335,6 +352,50 @@ function loadLayoutServerHook(layoutFile) {
335
352
  }
336
353
 
337
354
  // modules/router/loader-pages.ts
355
+ function validateRoutes(routes, appDir) {
356
+ const routePatterns = /* @__PURE__ */ new Map();
357
+ const errors = [];
358
+ const warnings = [];
359
+ for (const route of routes) {
360
+ const existing = routePatterns.get(route.pattern) || [];
361
+ existing.push(route);
362
+ routePatterns.set(route.pattern, existing);
363
+ }
364
+ for (const [pattern, duplicateRoutes] of routePatterns.entries()) {
365
+ if (duplicateRoutes.length > 1) {
366
+ const files = duplicateRoutes.map(
367
+ (r) => r.pageFile ? import_path4.default.relative(appDir, r.pageFile) : "unknown"
368
+ ).join(", ");
369
+ errors.push(
370
+ `Duplicate route pattern "${pattern}" found in multiple files:
371
+ ${files}
372
+ \u{1F4A1} Suggestion: Ensure each route has a unique path pattern`
373
+ );
374
+ }
375
+ }
376
+ for (const route of routes) {
377
+ if (!route.pageFile || !import_fs4.default.existsSync(route.pageFile)) {
378
+ warnings.push(
379
+ `Route pattern "${route.pattern}" references a missing page file`
380
+ );
381
+ }
382
+ }
383
+ if (errors.length > 0) {
384
+ const errorMessage = [
385
+ "\u274C Route validation failed:",
386
+ "",
387
+ ...errors,
388
+ "",
389
+ "\u{1F4A1} Please fix the errors above before starting the server."
390
+ ].join("\n");
391
+ throw new Error(errorMessage);
392
+ }
393
+ if (warnings.length > 0 && process.env.NODE_ENV === "development") {
394
+ console.warn("\n\u26A0\uFE0F Route warnings:");
395
+ warnings.forEach((warning) => console.warn(` \u2022 ${warning}`));
396
+ console.warn("");
397
+ }
398
+ }
338
399
  function loadRoutes(appDir) {
339
400
  if (!import_fs4.default.existsSync(appDir)) {
340
401
  return [];
@@ -389,6 +450,7 @@ function loadRoutes(appDir) {
389
450
  }
390
451
  }
391
452
  walk(appDir);
453
+ validateRoutes(routes, appDir);
392
454
  return routes;
393
455
  }
394
456
 
@@ -752,9 +814,12 @@ function writeClientBoostrapManifest(projectRoot) {
752
814
  lines.push("");
753
815
  lines.push(`import { bootstrapClient } from "@lolyjs/core/runtime"`);
754
816
  lines.push("");
755
- lines.push(
756
- "bootstrapClient(routes as ClientRouteLoaded[], notFoundRoute, errorRoute);"
757
- );
817
+ lines.push(`try {`);
818
+ lines.push(` bootstrapClient(routes as ClientRouteLoaded[], notFoundRoute, errorRoute);`);
819
+ lines.push(`} catch (error) {`);
820
+ lines.push(` console.error("[bootstrap] Fatal error during bootstrap:", error);`);
821
+ lines.push(` throw error;`);
822
+ lines.push(`}`);
758
823
  import_fs7.default.writeFileSync(manifestPath, lines.join("\n"), "utf-8");
759
824
  }
760
825
  function writeRoutesManifest({
@@ -1092,24 +1157,210 @@ function loadWssRoutes(appDir) {
1092
1157
  }
1093
1158
 
1094
1159
  // modules/router/route-loader.ts
1160
+ var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
1161
+ "node_modules",
1162
+ ".git",
1163
+ ".loly",
1164
+ "dist",
1165
+ "build",
1166
+ ".next",
1167
+ ".cache",
1168
+ "coverage",
1169
+ ".vscode",
1170
+ ".idea",
1171
+ ".turbo",
1172
+ ".swc"
1173
+ ]);
1174
+ function getRelevantFiles(appDir, projectRoot) {
1175
+ const files = [];
1176
+ const projectRootNormalized = import_path12.default.resolve(projectRoot);
1177
+ function walk(currentDir) {
1178
+ if (!import_fs10.default.existsSync(currentDir)) {
1179
+ return;
1180
+ }
1181
+ const entries = import_fs10.default.readdirSync(currentDir, { withFileTypes: true });
1182
+ for (const entry of entries) {
1183
+ const fullPath = import_path12.default.join(currentDir, entry.name);
1184
+ if (entry.isDirectory()) {
1185
+ if (SKIP_DIRECTORIES.has(entry.name)) {
1186
+ continue;
1187
+ }
1188
+ if (entry.name.startsWith(".")) {
1189
+ continue;
1190
+ }
1191
+ walk(fullPath);
1192
+ } else {
1193
+ const ext = import_path12.default.extname(entry.name);
1194
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
1195
+ files.push(fullPath);
1196
+ }
1197
+ }
1198
+ }
1199
+ }
1200
+ walk(projectRootNormalized);
1201
+ return files;
1202
+ }
1203
+ function hasFilesChanged(appDir, projectRoot, cachedStats) {
1204
+ const currentFiles = getRelevantFiles(appDir, projectRoot);
1205
+ const currentFilesSet = new Set(currentFiles);
1206
+ for (const [filePath] of cachedStats.entries()) {
1207
+ if (!currentFilesSet.has(filePath)) {
1208
+ return true;
1209
+ }
1210
+ }
1211
+ for (const filePath of currentFiles) {
1212
+ if (!import_fs10.default.existsSync(filePath)) {
1213
+ continue;
1214
+ }
1215
+ const stats = import_fs10.default.statSync(filePath);
1216
+ const cachedStat = cachedStats.get(filePath);
1217
+ if (!cachedStat) {
1218
+ return true;
1219
+ }
1220
+ if (stats.mtimeMs !== cachedStat.mtime || stats.size !== cachedStat.size) {
1221
+ return true;
1222
+ }
1223
+ }
1224
+ return false;
1225
+ }
1226
+ function buildFileStats(files) {
1227
+ const statsMap = /* @__PURE__ */ new Map();
1228
+ for (const filePath of files) {
1229
+ if (import_fs10.default.existsSync(filePath)) {
1230
+ const stats = import_fs10.default.statSync(filePath);
1231
+ statsMap.set(filePath, {
1232
+ mtime: stats.mtimeMs,
1233
+ size: stats.size
1234
+ });
1235
+ }
1236
+ }
1237
+ return statsMap;
1238
+ }
1095
1239
  var FilesystemRouteLoader = class {
1096
- constructor(appDir) {
1240
+ // Maximum cache age in ms (1 second fallback)
1241
+ constructor(appDir, projectRoot = appDir) {
1097
1242
  this.appDir = appDir;
1243
+ this.projectRoot = projectRoot;
1244
+ this.cache = null;
1245
+ this.cacheMaxAge = 1e3;
1246
+ if (this.projectRoot === this.appDir) {
1247
+ let current = import_path12.default.resolve(this.appDir);
1248
+ while (current !== import_path12.default.dirname(current)) {
1249
+ if (import_fs10.default.existsSync(import_path12.default.join(current, "package.json"))) {
1250
+ this.projectRoot = current;
1251
+ break;
1252
+ }
1253
+ current = import_path12.default.dirname(current);
1254
+ }
1255
+ }
1256
+ }
1257
+ /**
1258
+ * Invalidates the cache, forcing a reload on next access.
1259
+ */
1260
+ invalidateCache() {
1261
+ this.cache = null;
1262
+ }
1263
+ /**
1264
+ * Checks if cache is still valid, invalidates if files changed.
1265
+ */
1266
+ ensureCacheValid() {
1267
+ if (!this.cache) {
1268
+ return;
1269
+ }
1270
+ const now = Date.now();
1271
+ if (now - this.cache.timestamp > this.cacheMaxAge) {
1272
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats)) {
1273
+ this.cache = null;
1274
+ } else {
1275
+ this.cache.timestamp = now;
1276
+ }
1277
+ }
1098
1278
  }
1099
1279
  loadRoutes() {
1100
- return loadRoutes(this.appDir);
1280
+ this.ensureCacheValid();
1281
+ if (!this.cache || hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats)) {
1282
+ const routes = loadRoutes(this.appDir);
1283
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1284
+ const fileStats = buildFileStats(files);
1285
+ this.cache = {
1286
+ routes,
1287
+ apiRoutes: this.cache?.apiRoutes || [],
1288
+ wssRoutes: this.cache?.wssRoutes || [],
1289
+ notFoundRoute: this.cache?.notFoundRoute ?? null,
1290
+ errorRoute: this.cache?.errorRoute ?? null,
1291
+ fileStats,
1292
+ timestamp: Date.now()
1293
+ };
1294
+ }
1295
+ return this.cache.routes;
1101
1296
  }
1102
1297
  loadApiRoutes() {
1103
- return loadApiRoutes(this.appDir);
1298
+ this.ensureCacheValid();
1299
+ if (!this.cache) {
1300
+ this.loadRoutes();
1301
+ }
1302
+ if (!this.cache) {
1303
+ throw new Error("Failed to initialize route cache");
1304
+ }
1305
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.apiRoutes.length === 0) {
1306
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1307
+ const fileStats = buildFileStats(files);
1308
+ this.cache.apiRoutes = loadApiRoutes(this.appDir);
1309
+ this.cache.fileStats = fileStats;
1310
+ this.cache.timestamp = Date.now();
1311
+ }
1312
+ return this.cache.apiRoutes;
1104
1313
  }
1105
1314
  loadWssRoutes() {
1106
- return loadWssRoutes(this.appDir);
1315
+ this.ensureCacheValid();
1316
+ if (!this.cache) {
1317
+ this.loadRoutes();
1318
+ }
1319
+ if (!this.cache) {
1320
+ throw new Error("Failed to initialize route cache");
1321
+ }
1322
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.wssRoutes.length === 0) {
1323
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1324
+ const fileStats = buildFileStats(files);
1325
+ this.cache.wssRoutes = loadWssRoutes(this.appDir);
1326
+ this.cache.fileStats = fileStats;
1327
+ this.cache.timestamp = Date.now();
1328
+ }
1329
+ return this.cache.wssRoutes;
1107
1330
  }
1108
1331
  loadNotFoundRoute() {
1109
- return loadNotFoundRouteFromFilesystem(this.appDir);
1332
+ this.ensureCacheValid();
1333
+ if (!this.cache) {
1334
+ this.loadRoutes();
1335
+ }
1336
+ if (!this.cache) {
1337
+ throw new Error("Failed to initialize route cache");
1338
+ }
1339
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.notFoundRoute === void 0) {
1340
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1341
+ const fileStats = buildFileStats(files);
1342
+ this.cache.notFoundRoute = loadNotFoundRouteFromFilesystem(this.appDir);
1343
+ this.cache.fileStats = fileStats;
1344
+ this.cache.timestamp = Date.now();
1345
+ }
1346
+ return this.cache.notFoundRoute;
1110
1347
  }
1111
1348
  loadErrorRoute() {
1112
- return loadErrorRouteFromFilesystem(this.appDir);
1349
+ this.ensureCacheValid();
1350
+ if (!this.cache) {
1351
+ this.loadRoutes();
1352
+ }
1353
+ if (!this.cache) {
1354
+ throw new Error("Failed to initialize route cache");
1355
+ }
1356
+ if (hasFilesChanged(this.appDir, this.projectRoot, this.cache.fileStats) || this.cache.errorRoute === void 0) {
1357
+ const files = getRelevantFiles(this.appDir, this.projectRoot);
1358
+ const fileStats = buildFileStats(files);
1359
+ this.cache.errorRoute = loadErrorRouteFromFilesystem(this.appDir);
1360
+ this.cache.fileStats = fileStats;
1361
+ this.cache.timestamp = Date.now();
1362
+ }
1363
+ return this.cache.errorRoute;
1113
1364
  }
1114
1365
  loadRouteChunks() {
1115
1366
  return {};
@@ -1118,27 +1369,67 @@ var FilesystemRouteLoader = class {
1118
1369
  var ManifestRouteLoader = class {
1119
1370
  constructor(projectRoot) {
1120
1371
  this.projectRoot = projectRoot;
1372
+ this.cache = {};
1373
+ }
1374
+ /**
1375
+ * Gets the manifest, using cache if available.
1376
+ * The manifest is read once and cached for the lifetime of the loader.
1377
+ */
1378
+ getManifest() {
1379
+ if (this.cache.manifest !== void 0) {
1380
+ return this.cache.manifest;
1381
+ }
1382
+ const manifest = readManifest(this.projectRoot);
1383
+ this.cache.manifest = manifest;
1384
+ return manifest;
1121
1385
  }
1122
1386
  loadRoutes() {
1387
+ if (this.cache.routes) {
1388
+ return this.cache.routes;
1389
+ }
1123
1390
  const { routes } = loadRoutesFromManifest(this.projectRoot);
1391
+ this.cache.routes = routes;
1124
1392
  return routes;
1125
1393
  }
1126
1394
  loadApiRoutes() {
1395
+ if (this.cache.apiRoutes) {
1396
+ return this.cache.apiRoutes;
1397
+ }
1127
1398
  const { apiRoutes } = loadRoutesFromManifest(this.projectRoot);
1399
+ this.cache.apiRoutes = apiRoutes;
1128
1400
  return apiRoutes;
1129
1401
  }
1130
1402
  loadWssRoutes() {
1403
+ if (this.cache.wssRoutes) {
1404
+ return this.cache.wssRoutes;
1405
+ }
1131
1406
  const { wssRoutes } = loadRoutesFromManifest(this.projectRoot);
1407
+ this.cache.wssRoutes = wssRoutes;
1132
1408
  return wssRoutes;
1133
1409
  }
1134
1410
  loadNotFoundRoute() {
1135
- return loadNotFoundFromManifest(this.projectRoot);
1411
+ if (this.cache.notFoundRoute !== void 0) {
1412
+ return this.cache.notFoundRoute;
1413
+ }
1414
+ const route = loadNotFoundFromManifest(this.projectRoot);
1415
+ this.cache.notFoundRoute = route;
1416
+ return route;
1136
1417
  }
1137
1418
  loadErrorRoute() {
1138
- return loadErrorFromManifest(this.projectRoot);
1419
+ if (this.cache.errorRoute !== void 0) {
1420
+ return this.cache.errorRoute;
1421
+ }
1422
+ const route = loadErrorFromManifest(this.projectRoot);
1423
+ this.cache.errorRoute = route;
1424
+ return route;
1139
1425
  }
1140
1426
  loadRouteChunks() {
1141
- return loadChunksFromManifest(this.projectRoot);
1427
+ if (this.cache.routeChunks) {
1428
+ return this.cache.routeChunks;
1429
+ }
1430
+ const chunks = loadChunksFromManifest(this.projectRoot);
1431
+ this.cache.routeChunks = chunks;
1432
+ return chunks;
1142
1433
  }
1143
1434
  };
1144
1435
  function loadNotFoundRouteFromFilesystem(appDir) {
@@ -1271,7 +1562,8 @@ function loadAliasesFromTsconfig(projectRoot) {
1271
1562
  try {
1272
1563
  tsconfig = JSON.parse(import_fs11.default.readFileSync(tsconfigPath, "utf-8"));
1273
1564
  } catch (err) {
1274
- console.warn("[framework] Could not read tsconfig.json:", err);
1565
+ console.warn("\u26A0\uFE0F [framework] Could not read tsconfig.json:", err instanceof Error ? err.message : String(err));
1566
+ console.warn("\u{1F4A1} Using default path aliases. For custom aliases, ensure tsconfig.json is valid.");
1275
1567
  aliases["@app"] = import_path13.default.resolve(projectRoot, "app");
1276
1568
  return aliases;
1277
1569
  }
@@ -1325,7 +1617,7 @@ function copyStaticAssets(projectRoot, outDir) {
1325
1617
  }
1326
1618
  }
1327
1619
  }
1328
- function generateAssetManifest(outDir) {
1620
+ function generateAssetManifest(outDir, stats) {
1329
1621
  const manifest = {
1330
1622
  client: {
1331
1623
  js: "client.js",
@@ -1337,27 +1629,128 @@ function generateAssetManifest(outDir) {
1337
1629
  return manifest;
1338
1630
  }
1339
1631
  const files = import_fs11.default.readdirSync(outDir);
1340
- const clientJsMatch = files.find((f) => /^client\.[\w-]+\.js$/.test(f) || f === "client.js");
1341
- if (clientJsMatch) {
1342
- manifest.client.js = clientJsMatch;
1632
+ if (stats) {
1633
+ try {
1634
+ const statsJson = stats.toJson({
1635
+ all: false,
1636
+ entrypoints: true,
1637
+ assets: true,
1638
+ chunks: true,
1639
+ chunkRelations: true
1640
+ // Include chunk dependencies
1641
+ });
1642
+ const clientEntrypoint = statsJson.entrypoints?.client;
1643
+ if (clientEntrypoint?.assets) {
1644
+ const clientJsFiles = clientEntrypoint.assets.map((asset) => typeof asset === "string" ? asset : asset.name).filter((name) => name.endsWith(".js"));
1645
+ if (statsJson.chunks && clientEntrypoint.chunks) {
1646
+ const entrypointChunkIds = new Set(
1647
+ Array.isArray(clientEntrypoint.chunks) ? clientEntrypoint.chunks : [clientEntrypoint.chunks]
1648
+ );
1649
+ const dependencyChunks = [];
1650
+ for (const chunk of statsJson.chunks) {
1651
+ if (chunk.id && entrypointChunkIds.has(chunk.id)) {
1652
+ if (chunk.files) {
1653
+ const jsFiles = chunk.files.filter((f) => f.endsWith(".js")).filter((f) => !clientJsFiles.includes(f));
1654
+ dependencyChunks.push(...jsFiles);
1655
+ }
1656
+ }
1657
+ }
1658
+ const visitedChunkIds = new Set(entrypointChunkIds);
1659
+ const chunksToCheck = Array.from(entrypointChunkIds);
1660
+ while (chunksToCheck.length > 0) {
1661
+ const chunkId = chunksToCheck.shift();
1662
+ if (!chunkId) continue;
1663
+ const chunk = statsJson.chunks?.find((c) => c.id === chunkId);
1664
+ if (chunk?.children) {
1665
+ const children = Array.isArray(chunk.children) ? chunk.children : [chunk.children];
1666
+ for (const childId of children) {
1667
+ if (!visitedChunkIds.has(childId)) {
1668
+ visitedChunkIds.add(childId);
1669
+ chunksToCheck.push(childId);
1670
+ const childChunk = statsJson.chunks?.find((c) => c.id === childId);
1671
+ if (childChunk?.files) {
1672
+ const jsFiles = childChunk.files.filter((f) => f.endsWith(".js")).filter((f) => !clientJsFiles.includes(f) && !dependencyChunks.includes(f));
1673
+ dependencyChunks.push(...jsFiles);
1674
+ }
1675
+ }
1676
+ }
1677
+ }
1678
+ }
1679
+ if (dependencyChunks.length > 0) {
1680
+ clientJsFiles.splice(-1, 0, ...dependencyChunks);
1681
+ }
1682
+ }
1683
+ if (clientJsFiles.length > 0) {
1684
+ manifest.entrypoints = {
1685
+ client: clientJsFiles
1686
+ };
1687
+ manifest.client.js = clientJsFiles[clientJsFiles.length - 1];
1688
+ const clientCssFiles = clientEntrypoint.assets.map((asset) => typeof asset === "string" ? asset : asset.name).filter((name) => name.endsWith(".css"));
1689
+ if (clientCssFiles.length > 0) {
1690
+ manifest.client.css = clientCssFiles[0];
1691
+ }
1692
+ }
1693
+ }
1694
+ } catch (err) {
1695
+ console.warn("[framework] Failed to extract entrypoints from stats, falling back to file scanning:", err);
1696
+ }
1343
1697
  }
1344
- const clientCssMatch = files.find((f) => /^client\.[\w-]+\.css$/.test(f) || f === "client.css");
1345
- if (clientCssMatch) {
1346
- manifest.client.css = clientCssMatch;
1698
+ if (!manifest.client.js) {
1699
+ const clientJsMatch = files.find((f) => /^client\.[\w-]+\.js$/.test(f) || f === "client.js");
1700
+ if (clientJsMatch) {
1701
+ manifest.client.js = clientJsMatch;
1702
+ }
1703
+ }
1704
+ if (!manifest.client.css) {
1705
+ const clientCssMatch = files.find((f) => /^client\.[\w-]+\.css$/.test(f) || f === "client.css");
1706
+ if (clientCssMatch) {
1707
+ manifest.client.css = clientCssMatch;
1708
+ }
1347
1709
  }
1710
+ const sharedChunksToAdd = [];
1348
1711
  for (const file of files) {
1349
1712
  if (!file.endsWith(".js")) continue;
1350
1713
  if (file === manifest.client.js) continue;
1714
+ if (manifest.entrypoints?.client?.includes(file)) continue;
1351
1715
  const routeMatch = file.match(/^(route-[^.]+)(\.[\w-]+)?\.js$/);
1352
1716
  if (routeMatch) {
1353
1717
  const chunkName = routeMatch[1];
1354
1718
  manifest.chunks[chunkName] = file;
1355
1719
  continue;
1356
1720
  }
1721
+ const vendorMatch = file.match(/^(vendor)(\.[\w-]+)?\.js$/);
1722
+ if (vendorMatch) {
1723
+ const chunkName = vendorMatch[1];
1724
+ manifest.chunks[chunkName] = file;
1725
+ sharedChunksToAdd.push(file);
1726
+ continue;
1727
+ }
1728
+ const vendorCommonsMatch = file.match(/^(vendor-commons)(\.[\w-]+)?\.js$/);
1729
+ if (vendorCommonsMatch) {
1730
+ const chunkName = vendorCommonsMatch[1];
1731
+ manifest.chunks[chunkName] = file;
1732
+ sharedChunksToAdd.push(file);
1733
+ continue;
1734
+ }
1357
1735
  const numericMatch = file.match(/^(\d+)(\.[\w-]+)?\.js$/);
1358
1736
  if (numericMatch) {
1359
1737
  const chunkName = numericMatch[1];
1360
1738
  manifest.chunks[chunkName] = file;
1739
+ sharedChunksToAdd.push(file);
1740
+ continue;
1741
+ }
1742
+ }
1743
+ if (sharedChunksToAdd.length > 0 && manifest.entrypoints?.client) {
1744
+ const entrypoints = manifest.entrypoints.client;
1745
+ const mainEntry = entrypoints[entrypoints.length - 1];
1746
+ const uniqueShared = sharedChunksToAdd.filter((f) => !entrypoints.includes(f));
1747
+ entrypoints.splice(-1, 0, ...uniqueShared);
1748
+ if (entrypoints[entrypoints.length - 1] !== mainEntry) {
1749
+ const mainIndex = entrypoints.indexOf(mainEntry);
1750
+ if (mainIndex >= 0) {
1751
+ entrypoints.splice(mainIndex, 1);
1752
+ }
1753
+ entrypoints.push(mainEntry);
1361
1754
  }
1362
1755
  }
1363
1756
  return manifest;
@@ -1397,9 +1790,7 @@ function createClientConfig(projectRoot, mode) {
1397
1790
  const outDir = import_path14.default.join(buildDir, "client");
1398
1791
  const envPath2 = import_path14.default.join(projectRoot, ".env");
1399
1792
  if (import_fs12.default.existsSync(envPath2)) {
1400
- import_dotenv.default.config({
1401
- path: envPath2
1402
- });
1793
+ import_dotenv.default.config({ path: envPath2 });
1403
1794
  }
1404
1795
  const publicEnv = {};
1405
1796
  for (const [key, value] of Object.entries(process.env)) {
@@ -1458,14 +1849,57 @@ function createClientConfig(projectRoot, mode) {
1458
1849
  },
1459
1850
  plugins: [
1460
1851
  new import_core.rspack.DefinePlugin({
1461
- // Always define NODE_ENV, using mode as fallback if not set
1462
- "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || mode),
1852
+ // Use mode directly to ensure development mode is correctly set
1853
+ // This replaces process.env.NODE_ENV in the client bundle with the literal string value
1854
+ "process.env.NODE_ENV": JSON.stringify(mode),
1463
1855
  ...publicEnv
1464
1856
  }),
1465
1857
  new import_core.rspack.CssExtractRspackPlugin({
1466
1858
  filename: mode === "production" ? "client.[contenthash].css" : "client.css"
1467
1859
  })
1468
1860
  ],
1861
+ optimization: mode === "production" ? {
1862
+ usedExports: true,
1863
+ sideEffects: false,
1864
+ // More aggressive tree shaking - assume no side effects
1865
+ providedExports: true,
1866
+ concatenateModules: true,
1867
+ // Better for tree shaking
1868
+ minimize: true,
1869
+ removeEmptyChunks: true,
1870
+ mergeDuplicateChunks: true,
1871
+ // Improved code splitting: separate vendor chunks for better caching
1872
+ splitChunks: {
1873
+ chunks: "all",
1874
+ cacheGroups: {
1875
+ // Separate React/React-DOM into dedicated vendor chunk
1876
+ // This improves caching: React rarely changes, so users don't need to re-download it
1877
+ vendor: {
1878
+ test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
1879
+ name: "vendor",
1880
+ priority: 30,
1881
+ enforce: true,
1882
+ // Force separation even if only used once
1883
+ reuseExistingChunk: true
1884
+ },
1885
+ // Other node_modules dependencies in a separate chunk
1886
+ vendorCommons: {
1887
+ test: /[\\/]node_modules[\\/](?!(react|react-dom|scheduler)[\\/])/,
1888
+ name: "vendor-commons",
1889
+ priority: 10,
1890
+ minChunks: 2,
1891
+ // Only create if used in 2+ chunks
1892
+ reuseExistingChunk: true
1893
+ },
1894
+ // Default: shared application code (not in node_modules)
1895
+ default: {
1896
+ minChunks: 2,
1897
+ priority: 5,
1898
+ reuseExistingChunk: true
1899
+ }
1900
+ }
1901
+ }
1902
+ } : void 0,
1469
1903
  infrastructureLogging: {
1470
1904
  level: "error"
1471
1905
  },
@@ -1494,7 +1928,13 @@ function startClientBundler(projectRoot, mode = "development") {
1494
1928
  });
1495
1929
  compiler.watch({}, (err, stats) => {
1496
1930
  if (err) {
1497
- console.error("[framework][client] Rspack error:", err);
1931
+ console.error("\n\u274C [framework][client] Rspack compilation error:");
1932
+ console.error(err);
1933
+ console.error("\n\u{1F4A1} Suggestions:");
1934
+ console.error(" \u2022 Check for syntax errors in your code");
1935
+ console.error(" \u2022 Verify all imports are correct");
1936
+ console.error(" \u2022 Ensure all dependencies are installed");
1937
+ console.error(" \u2022 Try deleting .loly folder and rebuilding\n");
1498
1938
  isBuilding = false;
1499
1939
  lastBuildTime = Date.now();
1500
1940
  if (buildResolve) {
@@ -1505,17 +1945,20 @@ function startClientBundler(projectRoot, mode = "development") {
1505
1945
  return;
1506
1946
  }
1507
1947
  if (!stats) {
1948
+ console.warn("\u26A0\uFE0F [framework][client] Build completed but no stats available");
1508
1949
  isBuilding = false;
1509
1950
  lastBuildTime = Date.now();
1510
1951
  return;
1511
1952
  }
1512
1953
  if (stats.hasErrors()) {
1513
- console.error(
1514
- "[framework][client] Build with errors:\n",
1515
- stats.toString("errors-only")
1516
- );
1954
+ console.error("\n\u274C [framework][client] Build failed with errors:\n");
1955
+ console.error(stats.toString("errors-only"));
1956
+ console.error("\n\u{1F4A1} Common fixes:");
1957
+ console.error(" \u2022 Fix syntax errors shown above");
1958
+ console.error(" \u2022 Check for missing imports or dependencies");
1959
+ console.error(" \u2022 Verify TypeScript types are correct\n");
1517
1960
  } else {
1518
- console.log("[framework][client] \u2713 Client bundle rebuilt successfully");
1961
+ console.log("\u2705 [framework][client] Client bundle rebuilt successfully");
1519
1962
  }
1520
1963
  isBuilding = false;
1521
1964
  lastBuildTime = Date.now();
@@ -1550,23 +1993,33 @@ function buildClientBundle(projectRoot) {
1550
1993
  compiler.close(() => {
1551
1994
  });
1552
1995
  if (err) {
1553
- console.error("[framework][client] Build error:", err);
1996
+ console.error("\n\u274C [framework][client] Production build error:");
1997
+ console.error(err);
1998
+ console.error("\n\u{1F4A1} Suggestions:");
1999
+ console.error(" \u2022 Check for syntax errors in your code");
2000
+ console.error(" \u2022 Verify all imports are correct");
2001
+ console.error(" \u2022 Ensure all dependencies are installed");
2002
+ console.error(" \u2022 Review the error details above\n");
1554
2003
  return reject(err);
1555
2004
  }
1556
2005
  if (!stats) {
1557
- const error = new Error("No stats from Rspack");
1558
- console.error("[framework][client] Build error:", error);
2006
+ const error = new Error("No stats from Rspack - build may have failed silently");
2007
+ console.error("\n\u274C [framework][client] Build error:", error.message);
2008
+ console.error("\u{1F4A1} Try rebuilding or check Rspack configuration\n");
1559
2009
  return reject(error);
1560
2010
  }
1561
2011
  if (stats.hasErrors()) {
1562
- console.error(
1563
- "[framework][client] Build with errors:\n",
1564
- stats.toString("errors-only")
1565
- );
1566
- return reject(new Error("Client build failed"));
2012
+ console.error("\n\u274C [framework][client] Production build failed:\n");
2013
+ console.error(stats.toString("errors-only"));
2014
+ console.error("\n\u{1F4A1} Common fixes:");
2015
+ console.error(" \u2022 Fix syntax errors shown above");
2016
+ console.error(" \u2022 Check for missing imports or dependencies");
2017
+ console.error(" \u2022 Verify TypeScript types are correct");
2018
+ console.error(" \u2022 Review build configuration\n");
2019
+ return reject(new Error("Client build failed - see errors above"));
1567
2020
  }
1568
2021
  copyStaticAssets(projectRoot, outDir);
1569
- const assetManifest = generateAssetManifest(outDir);
2022
+ const assetManifest = generateAssetManifest(outDir, stats);
1570
2023
  const manifestPath = import_path15.default.join(projectRoot, BUILD_FOLDER_NAME, "asset-manifest.json");
1571
2024
  import_fs13.default.writeFileSync(manifestPath, JSON.stringify(assetManifest, null, 2), "utf-8");
1572
2025
  resolve3({ outDir });
@@ -1650,7 +2103,7 @@ var ReaddirpStream = class extends import_node_stream.Readable {
1650
2103
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
1651
2104
  const statMethod = opts.lstat ? import_promises.lstat : import_promises.stat;
1652
2105
  if (wantBigintFsStats) {
1653
- this._stat = (path25) => statMethod(path25, { bigint: true });
2106
+ this._stat = (path28) => statMethod(path28, { bigint: true });
1654
2107
  } else {
1655
2108
  this._stat = statMethod;
1656
2109
  }
@@ -1675,8 +2128,8 @@ var ReaddirpStream = class extends import_node_stream.Readable {
1675
2128
  const par = this.parent;
1676
2129
  const fil = par && par.files;
1677
2130
  if (fil && fil.length > 0) {
1678
- const { path: path25, depth } = par;
1679
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path25));
2131
+ const { path: path28, depth } = par;
2132
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path28));
1680
2133
  const awaited = await Promise.all(slice);
1681
2134
  for (const entry of awaited) {
1682
2135
  if (!entry)
@@ -1716,20 +2169,20 @@ var ReaddirpStream = class extends import_node_stream.Readable {
1716
2169
  this.reading = false;
1717
2170
  }
1718
2171
  }
1719
- async _exploreDir(path25, depth) {
2172
+ async _exploreDir(path28, depth) {
1720
2173
  let files;
1721
2174
  try {
1722
- files = await (0, import_promises.readdir)(path25, this._rdOptions);
2175
+ files = await (0, import_promises.readdir)(path28, this._rdOptions);
1723
2176
  } catch (error) {
1724
2177
  this._onError(error);
1725
2178
  }
1726
- return { files, depth, path: path25 };
2179
+ return { files, depth, path: path28 };
1727
2180
  }
1728
- async _formatEntry(dirent, path25) {
2181
+ async _formatEntry(dirent, path28) {
1729
2182
  let entry;
1730
2183
  const basename3 = this._isDirent ? dirent.name : dirent;
1731
2184
  try {
1732
- const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path25, basename3));
2185
+ const fullPath = (0, import_node_path.resolve)((0, import_node_path.join)(path28, basename3));
1733
2186
  entry = { path: (0, import_node_path.relative)(this._root, fullPath), fullPath, basename: basename3 };
1734
2187
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
1735
2188
  } catch (err) {
@@ -2129,16 +2582,16 @@ var delFromSet = (main, prop, item) => {
2129
2582
  };
2130
2583
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
2131
2584
  var FsWatchInstances = /* @__PURE__ */ new Map();
2132
- function createFsWatchInstance(path25, options, listener, errHandler, emitRaw) {
2585
+ function createFsWatchInstance(path28, options, listener, errHandler, emitRaw) {
2133
2586
  const handleEvent = (rawEvent, evPath) => {
2134
- listener(path25);
2135
- emitRaw(rawEvent, evPath, { watchedPath: path25 });
2136
- if (evPath && path25 !== evPath) {
2137
- fsWatchBroadcast(sysPath.resolve(path25, evPath), KEY_LISTENERS, sysPath.join(path25, evPath));
2587
+ listener(path28);
2588
+ emitRaw(rawEvent, evPath, { watchedPath: path28 });
2589
+ if (evPath && path28 !== evPath) {
2590
+ fsWatchBroadcast(sysPath.resolve(path28, evPath), KEY_LISTENERS, sysPath.join(path28, evPath));
2138
2591
  }
2139
2592
  };
2140
2593
  try {
2141
- return (0, import_fs14.watch)(path25, {
2594
+ return (0, import_fs14.watch)(path28, {
2142
2595
  persistent: options.persistent
2143
2596
  }, handleEvent);
2144
2597
  } catch (error) {
@@ -2154,12 +2607,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
2154
2607
  listener(val1, val2, val3);
2155
2608
  });
2156
2609
  };
2157
- var setFsWatchListener = (path25, fullPath, options, handlers) => {
2610
+ var setFsWatchListener = (path28, fullPath, options, handlers) => {
2158
2611
  const { listener, errHandler, rawEmitter } = handlers;
2159
2612
  let cont = FsWatchInstances.get(fullPath);
2160
2613
  let watcher;
2161
2614
  if (!options.persistent) {
2162
- watcher = createFsWatchInstance(path25, options, listener, errHandler, rawEmitter);
2615
+ watcher = createFsWatchInstance(path28, options, listener, errHandler, rawEmitter);
2163
2616
  if (!watcher)
2164
2617
  return;
2165
2618
  return watcher.close.bind(watcher);
@@ -2170,7 +2623,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2170
2623
  addAndConvert(cont, KEY_RAW, rawEmitter);
2171
2624
  } else {
2172
2625
  watcher = createFsWatchInstance(
2173
- path25,
2626
+ path28,
2174
2627
  options,
2175
2628
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
2176
2629
  errHandler,
@@ -2185,7 +2638,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2185
2638
  cont.watcherUnusable = true;
2186
2639
  if (isWindows && error.code === "EPERM") {
2187
2640
  try {
2188
- const fd = await (0, import_promises2.open)(path25, "r");
2641
+ const fd = await (0, import_promises2.open)(path28, "r");
2189
2642
  await fd.close();
2190
2643
  broadcastErr(error);
2191
2644
  } catch (err) {
@@ -2216,7 +2669,7 @@ var setFsWatchListener = (path25, fullPath, options, handlers) => {
2216
2669
  };
2217
2670
  };
2218
2671
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
2219
- var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
2672
+ var setFsWatchFileListener = (path28, fullPath, options, handlers) => {
2220
2673
  const { listener, rawEmitter } = handlers;
2221
2674
  let cont = FsWatchFileInstances.get(fullPath);
2222
2675
  const copts = cont && cont.options;
@@ -2238,7 +2691,7 @@ var setFsWatchFileListener = (path25, fullPath, options, handlers) => {
2238
2691
  });
2239
2692
  const currmtime = curr.mtimeMs;
2240
2693
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
2241
- foreach(cont.listeners, (listener2) => listener2(path25, curr));
2694
+ foreach(cont.listeners, (listener2) => listener2(path28, curr));
2242
2695
  }
2243
2696
  })
2244
2697
  };
@@ -2266,13 +2719,13 @@ var NodeFsHandler = class {
2266
2719
  * @param listener on fs change
2267
2720
  * @returns closer for the watcher instance
2268
2721
  */
2269
- _watchWithNodeFs(path25, listener) {
2722
+ _watchWithNodeFs(path28, listener) {
2270
2723
  const opts = this.fsw.options;
2271
- const directory = sysPath.dirname(path25);
2272
- const basename3 = sysPath.basename(path25);
2724
+ const directory = sysPath.dirname(path28);
2725
+ const basename3 = sysPath.basename(path28);
2273
2726
  const parent = this.fsw._getWatchedDir(directory);
2274
2727
  parent.add(basename3);
2275
- const absolutePath = sysPath.resolve(path25);
2728
+ const absolutePath = sysPath.resolve(path28);
2276
2729
  const options = {
2277
2730
  persistent: opts.persistent
2278
2731
  };
@@ -2282,12 +2735,12 @@ var NodeFsHandler = class {
2282
2735
  if (opts.usePolling) {
2283
2736
  const enableBin = opts.interval !== opts.binaryInterval;
2284
2737
  options.interval = enableBin && isBinaryPath(basename3) ? opts.binaryInterval : opts.interval;
2285
- closer = setFsWatchFileListener(path25, absolutePath, options, {
2738
+ closer = setFsWatchFileListener(path28, absolutePath, options, {
2286
2739
  listener,
2287
2740
  rawEmitter: this.fsw._emitRaw
2288
2741
  });
2289
2742
  } else {
2290
- closer = setFsWatchListener(path25, absolutePath, options, {
2743
+ closer = setFsWatchListener(path28, absolutePath, options, {
2291
2744
  listener,
2292
2745
  errHandler: this._boundHandleError,
2293
2746
  rawEmitter: this.fsw._emitRaw
@@ -2309,7 +2762,7 @@ var NodeFsHandler = class {
2309
2762
  let prevStats = stats;
2310
2763
  if (parent.has(basename3))
2311
2764
  return;
2312
- const listener = async (path25, newStats) => {
2765
+ const listener = async (path28, newStats) => {
2313
2766
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
2314
2767
  return;
2315
2768
  if (!newStats || newStats.mtimeMs === 0) {
@@ -2323,11 +2776,11 @@ var NodeFsHandler = class {
2323
2776
  this.fsw._emit(EV.CHANGE, file, newStats2);
2324
2777
  }
2325
2778
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
2326
- this.fsw._closeFile(path25);
2779
+ this.fsw._closeFile(path28);
2327
2780
  prevStats = newStats2;
2328
2781
  const closer2 = this._watchWithNodeFs(file, listener);
2329
2782
  if (closer2)
2330
- this.fsw._addPathCloser(path25, closer2);
2783
+ this.fsw._addPathCloser(path28, closer2);
2331
2784
  } else {
2332
2785
  prevStats = newStats2;
2333
2786
  }
@@ -2359,7 +2812,7 @@ var NodeFsHandler = class {
2359
2812
  * @param item basename of this item
2360
2813
  * @returns true if no more processing is needed for this entry.
2361
2814
  */
2362
- async _handleSymlink(entry, directory, path25, item) {
2815
+ async _handleSymlink(entry, directory, path28, item) {
2363
2816
  if (this.fsw.closed) {
2364
2817
  return;
2365
2818
  }
@@ -2369,7 +2822,7 @@ var NodeFsHandler = class {
2369
2822
  this.fsw._incrReadyCount();
2370
2823
  let linkPath;
2371
2824
  try {
2372
- linkPath = await (0, import_promises2.realpath)(path25);
2825
+ linkPath = await (0, import_promises2.realpath)(path28);
2373
2826
  } catch (e) {
2374
2827
  this.fsw._emitReady();
2375
2828
  return true;
@@ -2379,12 +2832,12 @@ var NodeFsHandler = class {
2379
2832
  if (dir.has(item)) {
2380
2833
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
2381
2834
  this.fsw._symlinkPaths.set(full, linkPath);
2382
- this.fsw._emit(EV.CHANGE, path25, entry.stats);
2835
+ this.fsw._emit(EV.CHANGE, path28, entry.stats);
2383
2836
  }
2384
2837
  } else {
2385
2838
  dir.add(item);
2386
2839
  this.fsw._symlinkPaths.set(full, linkPath);
2387
- this.fsw._emit(EV.ADD, path25, entry.stats);
2840
+ this.fsw._emit(EV.ADD, path28, entry.stats);
2388
2841
  }
2389
2842
  this.fsw._emitReady();
2390
2843
  return true;
@@ -2413,9 +2866,9 @@ var NodeFsHandler = class {
2413
2866
  return;
2414
2867
  }
2415
2868
  const item = entry.path;
2416
- let path25 = sysPath.join(directory, item);
2869
+ let path28 = sysPath.join(directory, item);
2417
2870
  current.add(item);
2418
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path25, item)) {
2871
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path28, item)) {
2419
2872
  return;
2420
2873
  }
2421
2874
  if (this.fsw.closed) {
@@ -2424,8 +2877,8 @@ var NodeFsHandler = class {
2424
2877
  }
2425
2878
  if (item === target || !target && !previous.has(item)) {
2426
2879
  this.fsw._incrReadyCount();
2427
- path25 = sysPath.join(dir, sysPath.relative(dir, path25));
2428
- this._addToNodeFs(path25, initialAdd, wh, depth + 1);
2880
+ path28 = sysPath.join(dir, sysPath.relative(dir, path28));
2881
+ this._addToNodeFs(path28, initialAdd, wh, depth + 1);
2429
2882
  }
2430
2883
  }).on(EV.ERROR, this._boundHandleError);
2431
2884
  return new Promise((resolve3, reject) => {
@@ -2494,13 +2947,13 @@ var NodeFsHandler = class {
2494
2947
  * @param depth Child path actually targeted for watch
2495
2948
  * @param target Child path actually targeted for watch
2496
2949
  */
2497
- async _addToNodeFs(path25, initialAdd, priorWh, depth, target) {
2950
+ async _addToNodeFs(path28, initialAdd, priorWh, depth, target) {
2498
2951
  const ready = this.fsw._emitReady;
2499
- if (this.fsw._isIgnored(path25) || this.fsw.closed) {
2952
+ if (this.fsw._isIgnored(path28) || this.fsw.closed) {
2500
2953
  ready();
2501
2954
  return false;
2502
2955
  }
2503
- const wh = this.fsw._getWatchHelpers(path25);
2956
+ const wh = this.fsw._getWatchHelpers(path28);
2504
2957
  if (priorWh) {
2505
2958
  wh.filterPath = (entry) => priorWh.filterPath(entry);
2506
2959
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -2516,8 +2969,8 @@ var NodeFsHandler = class {
2516
2969
  const follow = this.fsw.options.followSymlinks;
2517
2970
  let closer;
2518
2971
  if (stats.isDirectory()) {
2519
- const absPath = sysPath.resolve(path25);
2520
- const targetPath = follow ? await (0, import_promises2.realpath)(path25) : path25;
2972
+ const absPath = sysPath.resolve(path28);
2973
+ const targetPath = follow ? await (0, import_promises2.realpath)(path28) : path28;
2521
2974
  if (this.fsw.closed)
2522
2975
  return;
2523
2976
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -2527,29 +2980,29 @@ var NodeFsHandler = class {
2527
2980
  this.fsw._symlinkPaths.set(absPath, targetPath);
2528
2981
  }
2529
2982
  } else if (stats.isSymbolicLink()) {
2530
- const targetPath = follow ? await (0, import_promises2.realpath)(path25) : path25;
2983
+ const targetPath = follow ? await (0, import_promises2.realpath)(path28) : path28;
2531
2984
  if (this.fsw.closed)
2532
2985
  return;
2533
2986
  const parent = sysPath.dirname(wh.watchPath);
2534
2987
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
2535
2988
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
2536
- closer = await this._handleDir(parent, stats, initialAdd, depth, path25, wh, targetPath);
2989
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path28, wh, targetPath);
2537
2990
  if (this.fsw.closed)
2538
2991
  return;
2539
2992
  if (targetPath !== void 0) {
2540
- this.fsw._symlinkPaths.set(sysPath.resolve(path25), targetPath);
2993
+ this.fsw._symlinkPaths.set(sysPath.resolve(path28), targetPath);
2541
2994
  }
2542
2995
  } else {
2543
2996
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
2544
2997
  }
2545
2998
  ready();
2546
2999
  if (closer)
2547
- this.fsw._addPathCloser(path25, closer);
3000
+ this.fsw._addPathCloser(path28, closer);
2548
3001
  return false;
2549
3002
  } catch (error) {
2550
3003
  if (this.fsw._handleError(error)) {
2551
3004
  ready();
2552
- return path25;
3005
+ return path28;
2553
3006
  }
2554
3007
  }
2555
3008
  }
@@ -2592,26 +3045,26 @@ function createPattern(matcher) {
2592
3045
  }
2593
3046
  return () => false;
2594
3047
  }
2595
- function normalizePath(path25) {
2596
- if (typeof path25 !== "string")
3048
+ function normalizePath(path28) {
3049
+ if (typeof path28 !== "string")
2597
3050
  throw new Error("string expected");
2598
- path25 = sysPath2.normalize(path25);
2599
- path25 = path25.replace(/\\/g, "/");
3051
+ path28 = sysPath2.normalize(path28);
3052
+ path28 = path28.replace(/\\/g, "/");
2600
3053
  let prepend = false;
2601
- if (path25.startsWith("//"))
3054
+ if (path28.startsWith("//"))
2602
3055
  prepend = true;
2603
3056
  const DOUBLE_SLASH_RE2 = /\/\//;
2604
- while (path25.match(DOUBLE_SLASH_RE2))
2605
- path25 = path25.replace(DOUBLE_SLASH_RE2, "/");
3057
+ while (path28.match(DOUBLE_SLASH_RE2))
3058
+ path28 = path28.replace(DOUBLE_SLASH_RE2, "/");
2606
3059
  if (prepend)
2607
- path25 = "/" + path25;
2608
- return path25;
3060
+ path28 = "/" + path28;
3061
+ return path28;
2609
3062
  }
2610
3063
  function matchPatterns(patterns, testString, stats) {
2611
- const path25 = normalizePath(testString);
3064
+ const path28 = normalizePath(testString);
2612
3065
  for (let index = 0; index < patterns.length; index++) {
2613
3066
  const pattern = patterns[index];
2614
- if (pattern(path25, stats)) {
3067
+ if (pattern(path28, stats)) {
2615
3068
  return true;
2616
3069
  }
2617
3070
  }
@@ -2651,19 +3104,19 @@ var toUnix = (string) => {
2651
3104
  }
2652
3105
  return str;
2653
3106
  };
2654
- var normalizePathToUnix = (path25) => toUnix(sysPath2.normalize(toUnix(path25)));
2655
- var normalizeIgnored = (cwd = "") => (path25) => {
2656
- if (typeof path25 === "string") {
2657
- return normalizePathToUnix(sysPath2.isAbsolute(path25) ? path25 : sysPath2.join(cwd, path25));
3107
+ var normalizePathToUnix = (path28) => toUnix(sysPath2.normalize(toUnix(path28)));
3108
+ var normalizeIgnored = (cwd = "") => (path28) => {
3109
+ if (typeof path28 === "string") {
3110
+ return normalizePathToUnix(sysPath2.isAbsolute(path28) ? path28 : sysPath2.join(cwd, path28));
2658
3111
  } else {
2659
- return path25;
3112
+ return path28;
2660
3113
  }
2661
3114
  };
2662
- var getAbsolutePath = (path25, cwd) => {
2663
- if (sysPath2.isAbsolute(path25)) {
2664
- return path25;
3115
+ var getAbsolutePath = (path28, cwd) => {
3116
+ if (sysPath2.isAbsolute(path28)) {
3117
+ return path28;
2665
3118
  }
2666
- return sysPath2.join(cwd, path25);
3119
+ return sysPath2.join(cwd, path28);
2667
3120
  };
2668
3121
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
2669
3122
  var DirEntry = class {
@@ -2718,10 +3171,10 @@ var DirEntry = class {
2718
3171
  var STAT_METHOD_F = "stat";
2719
3172
  var STAT_METHOD_L = "lstat";
2720
3173
  var WatchHelper = class {
2721
- constructor(path25, follow, fsw) {
3174
+ constructor(path28, follow, fsw) {
2722
3175
  this.fsw = fsw;
2723
- const watchPath = path25;
2724
- this.path = path25 = path25.replace(REPLACER_RE, "");
3176
+ const watchPath = path28;
3177
+ this.path = path28 = path28.replace(REPLACER_RE, "");
2725
3178
  this.watchPath = watchPath;
2726
3179
  this.fullWatchPath = sysPath2.resolve(watchPath);
2727
3180
  this.dirParts = [];
@@ -2843,20 +3296,20 @@ var FSWatcher = class extends import_events.EventEmitter {
2843
3296
  this._closePromise = void 0;
2844
3297
  let paths = unifyPaths(paths_);
2845
3298
  if (cwd) {
2846
- paths = paths.map((path25) => {
2847
- const absPath = getAbsolutePath(path25, cwd);
3299
+ paths = paths.map((path28) => {
3300
+ const absPath = getAbsolutePath(path28, cwd);
2848
3301
  return absPath;
2849
3302
  });
2850
3303
  }
2851
- paths.forEach((path25) => {
2852
- this._removeIgnoredPath(path25);
3304
+ paths.forEach((path28) => {
3305
+ this._removeIgnoredPath(path28);
2853
3306
  });
2854
3307
  this._userIgnored = void 0;
2855
3308
  if (!this._readyCount)
2856
3309
  this._readyCount = 0;
2857
3310
  this._readyCount += paths.length;
2858
- Promise.all(paths.map(async (path25) => {
2859
- const res = await this._nodeFsHandler._addToNodeFs(path25, !_internal, void 0, 0, _origAdd);
3311
+ Promise.all(paths.map(async (path28) => {
3312
+ const res = await this._nodeFsHandler._addToNodeFs(path28, !_internal, void 0, 0, _origAdd);
2860
3313
  if (res)
2861
3314
  this._emitReady();
2862
3315
  return res;
@@ -2878,17 +3331,17 @@ var FSWatcher = class extends import_events.EventEmitter {
2878
3331
  return this;
2879
3332
  const paths = unifyPaths(paths_);
2880
3333
  const { cwd } = this.options;
2881
- paths.forEach((path25) => {
2882
- if (!sysPath2.isAbsolute(path25) && !this._closers.has(path25)) {
3334
+ paths.forEach((path28) => {
3335
+ if (!sysPath2.isAbsolute(path28) && !this._closers.has(path28)) {
2883
3336
  if (cwd)
2884
- path25 = sysPath2.join(cwd, path25);
2885
- path25 = sysPath2.resolve(path25);
3337
+ path28 = sysPath2.join(cwd, path28);
3338
+ path28 = sysPath2.resolve(path28);
2886
3339
  }
2887
- this._closePath(path25);
2888
- this._addIgnoredPath(path25);
2889
- if (this._watched.has(path25)) {
3340
+ this._closePath(path28);
3341
+ this._addIgnoredPath(path28);
3342
+ if (this._watched.has(path28)) {
2890
3343
  this._addIgnoredPath({
2891
- path: path25,
3344
+ path: path28,
2892
3345
  recursive: true
2893
3346
  });
2894
3347
  }
@@ -2952,38 +3405,38 @@ var FSWatcher = class extends import_events.EventEmitter {
2952
3405
  * @param stats arguments to be passed with event
2953
3406
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
2954
3407
  */
2955
- async _emit(event, path25, stats) {
3408
+ async _emit(event, path28, stats) {
2956
3409
  if (this.closed)
2957
3410
  return;
2958
3411
  const opts = this.options;
2959
3412
  if (isWindows)
2960
- path25 = sysPath2.normalize(path25);
3413
+ path28 = sysPath2.normalize(path28);
2961
3414
  if (opts.cwd)
2962
- path25 = sysPath2.relative(opts.cwd, path25);
2963
- const args = [path25];
3415
+ path28 = sysPath2.relative(opts.cwd, path28);
3416
+ const args = [path28];
2964
3417
  if (stats != null)
2965
3418
  args.push(stats);
2966
3419
  const awf = opts.awaitWriteFinish;
2967
3420
  let pw;
2968
- if (awf && (pw = this._pendingWrites.get(path25))) {
3421
+ if (awf && (pw = this._pendingWrites.get(path28))) {
2969
3422
  pw.lastChange = /* @__PURE__ */ new Date();
2970
3423
  return this;
2971
3424
  }
2972
3425
  if (opts.atomic) {
2973
3426
  if (event === EVENTS.UNLINK) {
2974
- this._pendingUnlinks.set(path25, [event, ...args]);
3427
+ this._pendingUnlinks.set(path28, [event, ...args]);
2975
3428
  setTimeout(() => {
2976
- this._pendingUnlinks.forEach((entry, path26) => {
3429
+ this._pendingUnlinks.forEach((entry, path29) => {
2977
3430
  this.emit(...entry);
2978
3431
  this.emit(EVENTS.ALL, ...entry);
2979
- this._pendingUnlinks.delete(path26);
3432
+ this._pendingUnlinks.delete(path29);
2980
3433
  });
2981
3434
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
2982
3435
  return this;
2983
3436
  }
2984
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path25)) {
3437
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path28)) {
2985
3438
  event = EVENTS.CHANGE;
2986
- this._pendingUnlinks.delete(path25);
3439
+ this._pendingUnlinks.delete(path28);
2987
3440
  }
2988
3441
  }
2989
3442
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -3001,16 +3454,16 @@ var FSWatcher = class extends import_events.EventEmitter {
3001
3454
  this.emitWithAll(event, args);
3002
3455
  }
3003
3456
  };
3004
- this._awaitWriteFinish(path25, awf.stabilityThreshold, event, awfEmit);
3457
+ this._awaitWriteFinish(path28, awf.stabilityThreshold, event, awfEmit);
3005
3458
  return this;
3006
3459
  }
3007
3460
  if (event === EVENTS.CHANGE) {
3008
- const isThrottled = !this._throttle(EVENTS.CHANGE, path25, 50);
3461
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path28, 50);
3009
3462
  if (isThrottled)
3010
3463
  return this;
3011
3464
  }
3012
3465
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
3013
- const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path25) : path25;
3466
+ const fullPath = opts.cwd ? sysPath2.join(opts.cwd, path28) : path28;
3014
3467
  let stats2;
3015
3468
  try {
3016
3469
  stats2 = await (0, import_promises3.stat)(fullPath);
@@ -3041,23 +3494,23 @@ var FSWatcher = class extends import_events.EventEmitter {
3041
3494
  * @param timeout duration of time to suppress duplicate actions
3042
3495
  * @returns tracking object or false if action should be suppressed
3043
3496
  */
3044
- _throttle(actionType, path25, timeout) {
3497
+ _throttle(actionType, path28, timeout) {
3045
3498
  if (!this._throttled.has(actionType)) {
3046
3499
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
3047
3500
  }
3048
3501
  const action = this._throttled.get(actionType);
3049
3502
  if (!action)
3050
3503
  throw new Error("invalid throttle");
3051
- const actionPath = action.get(path25);
3504
+ const actionPath = action.get(path28);
3052
3505
  if (actionPath) {
3053
3506
  actionPath.count++;
3054
3507
  return false;
3055
3508
  }
3056
3509
  let timeoutObject;
3057
3510
  const clear = () => {
3058
- const item = action.get(path25);
3511
+ const item = action.get(path28);
3059
3512
  const count = item ? item.count : 0;
3060
- action.delete(path25);
3513
+ action.delete(path28);
3061
3514
  clearTimeout(timeoutObject);
3062
3515
  if (item)
3063
3516
  clearTimeout(item.timeoutObject);
@@ -3065,7 +3518,7 @@ var FSWatcher = class extends import_events.EventEmitter {
3065
3518
  };
3066
3519
  timeoutObject = setTimeout(clear, timeout);
3067
3520
  const thr = { timeoutObject, clear, count: 0 };
3068
- action.set(path25, thr);
3521
+ action.set(path28, thr);
3069
3522
  return thr;
3070
3523
  }
3071
3524
  _incrReadyCount() {
@@ -3079,44 +3532,44 @@ var FSWatcher = class extends import_events.EventEmitter {
3079
3532
  * @param event
3080
3533
  * @param awfEmit Callback to be called when ready for event to be emitted.
3081
3534
  */
3082
- _awaitWriteFinish(path25, threshold, event, awfEmit) {
3535
+ _awaitWriteFinish(path28, threshold, event, awfEmit) {
3083
3536
  const awf = this.options.awaitWriteFinish;
3084
3537
  if (typeof awf !== "object")
3085
3538
  return;
3086
3539
  const pollInterval = awf.pollInterval;
3087
3540
  let timeoutHandler;
3088
- let fullPath = path25;
3089
- if (this.options.cwd && !sysPath2.isAbsolute(path25)) {
3090
- fullPath = sysPath2.join(this.options.cwd, path25);
3541
+ let fullPath = path28;
3542
+ if (this.options.cwd && !sysPath2.isAbsolute(path28)) {
3543
+ fullPath = sysPath2.join(this.options.cwd, path28);
3091
3544
  }
3092
3545
  const now = /* @__PURE__ */ new Date();
3093
3546
  const writes = this._pendingWrites;
3094
3547
  function awaitWriteFinishFn(prevStat) {
3095
3548
  (0, import_fs15.stat)(fullPath, (err, curStat) => {
3096
- if (err || !writes.has(path25)) {
3549
+ if (err || !writes.has(path28)) {
3097
3550
  if (err && err.code !== "ENOENT")
3098
3551
  awfEmit(err);
3099
3552
  return;
3100
3553
  }
3101
3554
  const now2 = Number(/* @__PURE__ */ new Date());
3102
3555
  if (prevStat && curStat.size !== prevStat.size) {
3103
- writes.get(path25).lastChange = now2;
3556
+ writes.get(path28).lastChange = now2;
3104
3557
  }
3105
- const pw = writes.get(path25);
3558
+ const pw = writes.get(path28);
3106
3559
  const df = now2 - pw.lastChange;
3107
3560
  if (df >= threshold) {
3108
- writes.delete(path25);
3561
+ writes.delete(path28);
3109
3562
  awfEmit(void 0, curStat);
3110
3563
  } else {
3111
3564
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
3112
3565
  }
3113
3566
  });
3114
3567
  }
3115
- if (!writes.has(path25)) {
3116
- writes.set(path25, {
3568
+ if (!writes.has(path28)) {
3569
+ writes.set(path28, {
3117
3570
  lastChange: now,
3118
3571
  cancelWait: () => {
3119
- writes.delete(path25);
3572
+ writes.delete(path28);
3120
3573
  clearTimeout(timeoutHandler);
3121
3574
  return event;
3122
3575
  }
@@ -3127,8 +3580,8 @@ var FSWatcher = class extends import_events.EventEmitter {
3127
3580
  /**
3128
3581
  * Determines whether user has asked to ignore this path.
3129
3582
  */
3130
- _isIgnored(path25, stats) {
3131
- if (this.options.atomic && DOT_RE.test(path25))
3583
+ _isIgnored(path28, stats) {
3584
+ if (this.options.atomic && DOT_RE.test(path28))
3132
3585
  return true;
3133
3586
  if (!this._userIgnored) {
3134
3587
  const { cwd } = this.options;
@@ -3138,17 +3591,17 @@ var FSWatcher = class extends import_events.EventEmitter {
3138
3591
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
3139
3592
  this._userIgnored = anymatch(list, void 0);
3140
3593
  }
3141
- return this._userIgnored(path25, stats);
3594
+ return this._userIgnored(path28, stats);
3142
3595
  }
3143
- _isntIgnored(path25, stat4) {
3144
- return !this._isIgnored(path25, stat4);
3596
+ _isntIgnored(path28, stat4) {
3597
+ return !this._isIgnored(path28, stat4);
3145
3598
  }
3146
3599
  /**
3147
3600
  * Provides a set of common helpers and properties relating to symlink handling.
3148
3601
  * @param path file or directory pattern being watched
3149
3602
  */
3150
- _getWatchHelpers(path25) {
3151
- return new WatchHelper(path25, this.options.followSymlinks, this);
3603
+ _getWatchHelpers(path28) {
3604
+ return new WatchHelper(path28, this.options.followSymlinks, this);
3152
3605
  }
3153
3606
  // Directory helpers
3154
3607
  // -----------------
@@ -3180,63 +3633,63 @@ var FSWatcher = class extends import_events.EventEmitter {
3180
3633
  * @param item base path of item/directory
3181
3634
  */
3182
3635
  _remove(directory, item, isDirectory) {
3183
- const path25 = sysPath2.join(directory, item);
3184
- const fullPath = sysPath2.resolve(path25);
3185
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path25) || this._watched.has(fullPath);
3186
- if (!this._throttle("remove", path25, 100))
3636
+ const path28 = sysPath2.join(directory, item);
3637
+ const fullPath = sysPath2.resolve(path28);
3638
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path28) || this._watched.has(fullPath);
3639
+ if (!this._throttle("remove", path28, 100))
3187
3640
  return;
3188
3641
  if (!isDirectory && this._watched.size === 1) {
3189
3642
  this.add(directory, item, true);
3190
3643
  }
3191
- const wp = this._getWatchedDir(path25);
3644
+ const wp = this._getWatchedDir(path28);
3192
3645
  const nestedDirectoryChildren = wp.getChildren();
3193
- nestedDirectoryChildren.forEach((nested) => this._remove(path25, nested));
3646
+ nestedDirectoryChildren.forEach((nested) => this._remove(path28, nested));
3194
3647
  const parent = this._getWatchedDir(directory);
3195
3648
  const wasTracked = parent.has(item);
3196
3649
  parent.remove(item);
3197
3650
  if (this._symlinkPaths.has(fullPath)) {
3198
3651
  this._symlinkPaths.delete(fullPath);
3199
3652
  }
3200
- let relPath = path25;
3653
+ let relPath = path28;
3201
3654
  if (this.options.cwd)
3202
- relPath = sysPath2.relative(this.options.cwd, path25);
3655
+ relPath = sysPath2.relative(this.options.cwd, path28);
3203
3656
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
3204
3657
  const event = this._pendingWrites.get(relPath).cancelWait();
3205
3658
  if (event === EVENTS.ADD)
3206
3659
  return;
3207
3660
  }
3208
- this._watched.delete(path25);
3661
+ this._watched.delete(path28);
3209
3662
  this._watched.delete(fullPath);
3210
3663
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
3211
- if (wasTracked && !this._isIgnored(path25))
3212
- this._emit(eventName, path25);
3213
- this._closePath(path25);
3664
+ if (wasTracked && !this._isIgnored(path28))
3665
+ this._emit(eventName, path28);
3666
+ this._closePath(path28);
3214
3667
  }
3215
3668
  /**
3216
3669
  * Closes all watchers for a path
3217
3670
  */
3218
- _closePath(path25) {
3219
- this._closeFile(path25);
3220
- const dir = sysPath2.dirname(path25);
3221
- this._getWatchedDir(dir).remove(sysPath2.basename(path25));
3671
+ _closePath(path28) {
3672
+ this._closeFile(path28);
3673
+ const dir = sysPath2.dirname(path28);
3674
+ this._getWatchedDir(dir).remove(sysPath2.basename(path28));
3222
3675
  }
3223
3676
  /**
3224
3677
  * Closes only file-specific watchers
3225
3678
  */
3226
- _closeFile(path25) {
3227
- const closers = this._closers.get(path25);
3679
+ _closeFile(path28) {
3680
+ const closers = this._closers.get(path28);
3228
3681
  if (!closers)
3229
3682
  return;
3230
3683
  closers.forEach((closer) => closer());
3231
- this._closers.delete(path25);
3684
+ this._closers.delete(path28);
3232
3685
  }
3233
- _addPathCloser(path25, closer) {
3686
+ _addPathCloser(path28, closer) {
3234
3687
  if (!closer)
3235
3688
  return;
3236
- let list = this._closers.get(path25);
3689
+ let list = this._closers.get(path28);
3237
3690
  if (!list) {
3238
3691
  list = [];
3239
- this._closers.set(path25, list);
3692
+ this._closers.set(path28, list);
3240
3693
  }
3241
3694
  list.push(closer);
3242
3695
  }
@@ -3271,54 +3724,132 @@ init_globals();
3271
3724
  function setupHotReload({
3272
3725
  app,
3273
3726
  appDir,
3727
+ projectRoot,
3274
3728
  route = "/__fw/hot",
3275
3729
  waitForBuild,
3276
3730
  onFileChange
3277
3731
  }) {
3278
3732
  const clients = /* @__PURE__ */ new Set();
3279
3733
  app.get(route, (req, res) => {
3280
- res.setHeader("Content-Type", "text/event-stream");
3281
- res.setHeader("Cache-Control", "no-cache");
3734
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
3735
+ res.setHeader("Cache-Control", "no-cache, no-transform");
3282
3736
  res.setHeader("Connection", "keep-alive");
3283
- res.flushHeaders?.();
3284
- res.write(`event: ping
3285
- data: connected
3286
-
3287
- `);
3737
+ res.setHeader("X-Accel-Buffering", "no");
3738
+ if (res.flushHeaders) {
3739
+ res.flushHeaders();
3740
+ } else {
3741
+ res.writeHead(200, {
3742
+ "Content-Type": "text/event-stream; charset=utf-8",
3743
+ "Cache-Control": "no-cache, no-transform",
3744
+ "Connection": "keep-alive",
3745
+ "X-Accel-Buffering": "no"
3746
+ });
3747
+ }
3288
3748
  clients.add(res);
3749
+ const pingMessage = "event: ping\ndata: connected\n\n";
3750
+ try {
3751
+ res.write(pingMessage, "utf-8");
3752
+ } catch (error) {
3753
+ console.error(`[hot-reload-server] \u274C Error sending ping:`, error);
3754
+ clients.delete(res);
3755
+ }
3289
3756
  req.on("close", () => {
3290
3757
  clients.delete(res);
3291
3758
  });
3759
+ req.on("aborted", () => {
3760
+ clients.delete(res);
3761
+ });
3292
3762
  });
3293
- const watcher = esm_default.watch(appDir, {
3763
+ console.log(`[hot-reload-server] \u2705 SSE endpoint registered at ${route}`);
3764
+ const resolvedProjectRoot = projectRoot ? import_path16.default.resolve(projectRoot) : import_path16.default.dirname(import_path16.default.resolve(appDir));
3765
+ const watcher = esm_default.watch(resolvedProjectRoot, {
3294
3766
  ignoreInitial: true,
3295
- ignored: ["**/node_modules/**", `**/${BUILD_FOLDER_NAME}/**`, "**/.git/**"]
3767
+ ignored: [
3768
+ "**/node_modules/**",
3769
+ `**/${BUILD_FOLDER_NAME}/**`,
3770
+ "**/.loly/**",
3771
+ // Ignore build output directory completely
3772
+ "**/.git/**",
3773
+ "**/dist/**",
3774
+ "**/build/**",
3775
+ "**/.next/**",
3776
+ "**/.cache/**",
3777
+ "**/.turbo/**",
3778
+ "**/.swc/**",
3779
+ "**/coverage/**",
3780
+ // Ignore generated files that trigger unnecessary reloads
3781
+ "**/*.map",
3782
+ // Source maps
3783
+ "**/*.log",
3784
+ // Log files
3785
+ "**/.DS_Store",
3786
+ // macOS
3787
+ "**/Thumbs.db"
3788
+ // Windows
3789
+ ],
3790
+ persistent: true,
3791
+ ignorePermissionErrors: true,
3792
+ // Only watch relevant source files (TypeScript, JavaScript, CSS)
3793
+ // Filter out JSON files in build directories
3794
+ awaitWriteFinish: {
3795
+ stabilityThreshold: 150,
3796
+ // Wait 150ms after file change to trigger event (debounce)
3797
+ pollInterval: 50
3798
+ // Check every 50ms
3799
+ }
3296
3800
  });
3801
+ let broadcastTimeout = null;
3802
+ const BROADCAST_DEBOUNCE_MS = 300;
3297
3803
  async function broadcastReload(reason, filePath) {
3804
+ const normalizedPath = import_path16.default.normalize(filePath);
3805
+ if (normalizedPath.includes(BUILD_FOLDER_NAME) || normalizedPath.includes(".loly") || normalizedPath.endsWith(".map") || normalizedPath.endsWith(".log") || normalizedPath.includes("route-chunks.json") || normalizedPath.includes("routes-client.ts") || normalizedPath.includes("/client/route-")) {
3806
+ return;
3807
+ }
3298
3808
  const rel = import_path16.default.relative(appDir, filePath);
3299
3809
  console.log(`[hot-reload] ${reason}: ${rel}`);
3300
- if (onFileChange) {
3301
- try {
3302
- await onFileChange(filePath);
3303
- } catch (error) {
3304
- console.warn("[hot-reload] Error in onFileChange callback:", error);
3305
- }
3810
+ if (broadcastTimeout) {
3811
+ clearTimeout(broadcastTimeout);
3306
3812
  }
3307
- if (waitForBuild) {
3308
- try {
3309
- console.log("[hot-reload] Waiting for client bundle to finish...");
3310
- await waitForBuild();
3311
- console.log("[hot-reload] Client bundle ready, sending reload event");
3312
- } catch (error) {
3313
- console.warn("[hot-reload] Error waiting for build:", error);
3813
+ broadcastTimeout = setTimeout(async () => {
3814
+ if (onFileChange) {
3815
+ try {
3816
+ await onFileChange(filePath);
3817
+ } catch (error) {
3818
+ console.warn("[hot-reload] Error in onFileChange callback:", error);
3819
+ }
3314
3820
  }
3315
- }
3316
- for (const res of clients) {
3317
- res.write(`event: message
3821
+ if (waitForBuild) {
3822
+ try {
3823
+ console.log("[hot-reload] Waiting for client bundle to finish...");
3824
+ await waitForBuild();
3825
+ console.log("[hot-reload] Client bundle ready, sending reload event");
3826
+ } catch (error) {
3827
+ console.warn("[hot-reload] Error waiting for build:", error);
3828
+ }
3829
+ }
3830
+ const message = `event: message
3318
3831
  data: reload:${rel}
3319
3832
 
3320
- `);
3321
- }
3833
+ `;
3834
+ console.log(`[hot-reload-server] \u{1F4E4} Broadcasting reload event to ${clients.size} client(s)`);
3835
+ let sentCount = 0;
3836
+ for (const res of clients) {
3837
+ try {
3838
+ if (res.writableEnded || res.destroyed) {
3839
+ clients.delete(res);
3840
+ continue;
3841
+ }
3842
+ res.write(message, "utf-8");
3843
+ sentCount++;
3844
+ } catch (error) {
3845
+ console.error(`[hot-reload-server] \u2717 Error sending to client:`, error);
3846
+ clients.delete(res);
3847
+ }
3848
+ }
3849
+ if (sentCount > 0) {
3850
+ console.log(`[hot-reload-server] \u2705 Reload event sent to ${sentCount} client(s)`);
3851
+ }
3852
+ }, BROADCAST_DEBOUNCE_MS);
3322
3853
  }
3323
3854
  watcher.on("add", (filePath) => broadcastReload("add", filePath)).on("change", (filePath) => broadcastReload("change", filePath)).on("unlink", (filePath) => broadcastReload("unlink", filePath));
3324
3855
  }
@@ -3394,6 +3925,121 @@ function deepMerge(target, source) {
3394
3925
  }
3395
3926
  return result;
3396
3927
  }
3928
+ var ConfigValidationError = class extends Error {
3929
+ constructor(message, errors = []) {
3930
+ super(message);
3931
+ this.errors = errors;
3932
+ this.name = "ConfigValidationError";
3933
+ }
3934
+ };
3935
+ function validateConfig(config, projectRoot) {
3936
+ const errors = [];
3937
+ if (!config.directories.app || typeof config.directories.app !== "string") {
3938
+ errors.push("config.directories.app must be a non-empty string");
3939
+ } else {
3940
+ const appDir = import_path18.default.join(projectRoot, config.directories.app);
3941
+ if (!import_fs16.default.existsSync(appDir) && process.env.NODE_ENV !== "test") {
3942
+ errors.push(
3943
+ `App directory not found: ${config.directories.app}
3944
+ Expected at: ${appDir}
3945
+ \u{1F4A1} Suggestion: Create the directory or update config.directories.app`
3946
+ );
3947
+ }
3948
+ }
3949
+ if (!config.directories.build || typeof config.directories.build !== "string") {
3950
+ errors.push("config.directories.build must be a non-empty string");
3951
+ }
3952
+ if (!config.directories.static || typeof config.directories.static !== "string") {
3953
+ errors.push("config.directories.static must be a non-empty string");
3954
+ }
3955
+ const conventionKeys = ["page", "layout", "notFound", "error", "api"];
3956
+ for (const key of conventionKeys) {
3957
+ if (!config.conventions[key] || typeof config.conventions[key] !== "string") {
3958
+ errors.push(`config.conventions.${key} must be a non-empty string`);
3959
+ }
3960
+ }
3961
+ if (!["always", "never", "ignore"].includes(config.routing.trailingSlash)) {
3962
+ errors.push(
3963
+ `config.routing.trailingSlash must be 'always', 'never', or 'ignore'
3964
+ Received: ${JSON.stringify(config.routing.trailingSlash)}
3965
+ \u{1F4A1} Suggestion: Use one of the valid values: 'always' | 'never' | 'ignore'`
3966
+ );
3967
+ }
3968
+ if (typeof config.routing.caseSensitive !== "boolean") {
3969
+ errors.push("config.routing.caseSensitive must be a boolean");
3970
+ }
3971
+ if (typeof config.routing.basePath !== "string") {
3972
+ errors.push("config.routing.basePath must be a string");
3973
+ } else if (config.routing.basePath && !config.routing.basePath.startsWith("/")) {
3974
+ errors.push(
3975
+ `config.routing.basePath must start with '/' (if not empty)
3976
+ Received: ${JSON.stringify(config.routing.basePath)}
3977
+ \u{1F4A1} Suggestion: Use an empty string '' or a path starting with '/', e.g., '/api'`
3978
+ );
3979
+ }
3980
+ const validClientBundlers = ["rspack", "webpack", "vite"];
3981
+ if (!validClientBundlers.includes(config.build.clientBundler)) {
3982
+ errors.push(
3983
+ `config.build.clientBundler must be one of: ${validClientBundlers.join(", ")}
3984
+ Received: ${JSON.stringify(config.build.clientBundler)}`
3985
+ );
3986
+ }
3987
+ const validServerBundlers = ["esbuild", "tsup", "swc"];
3988
+ if (!validServerBundlers.includes(config.build.serverBundler)) {
3989
+ errors.push(
3990
+ `config.build.serverBundler must be one of: ${validServerBundlers.join(", ")}
3991
+ Received: ${JSON.stringify(config.build.serverBundler)}`
3992
+ );
3993
+ }
3994
+ if (!["cjs", "esm"].includes(config.build.outputFormat)) {
3995
+ errors.push(
3996
+ `config.build.outputFormat must be 'cjs' or 'esm'
3997
+ Received: ${JSON.stringify(config.build.outputFormat)}`
3998
+ );
3999
+ }
4000
+ const validAdapters = ["express", "fastify", "koa"];
4001
+ if (!validAdapters.includes(config.server.adapter)) {
4002
+ errors.push(
4003
+ `config.server.adapter must be one of: ${validAdapters.join(", ")}
4004
+ Received: ${JSON.stringify(config.server.adapter)}`
4005
+ );
4006
+ }
4007
+ if (typeof config.server.port !== "number" || config.server.port < 1 || config.server.port > 65535) {
4008
+ errors.push(
4009
+ `config.server.port must be a number between 1 and 65535
4010
+ Received: ${JSON.stringify(config.server.port)}`
4011
+ );
4012
+ }
4013
+ if (!config.server.host || typeof config.server.host !== "string") {
4014
+ errors.push("config.server.host must be a non-empty string");
4015
+ }
4016
+ const validFrameworks = ["react", "preact", "vue", "svelte"];
4017
+ if (!validFrameworks.includes(config.rendering.framework)) {
4018
+ errors.push(
4019
+ `config.rendering.framework must be one of: ${validFrameworks.join(", ")}
4020
+ Received: ${JSON.stringify(config.rendering.framework)}`
4021
+ );
4022
+ }
4023
+ if (typeof config.rendering.streaming !== "boolean") {
4024
+ errors.push("config.rendering.streaming must be a boolean");
4025
+ }
4026
+ if (typeof config.rendering.ssr !== "boolean") {
4027
+ errors.push("config.rendering.ssr must be a boolean");
4028
+ }
4029
+ if (typeof config.rendering.ssg !== "boolean") {
4030
+ errors.push("config.rendering.ssg must be a boolean");
4031
+ }
4032
+ if (errors.length > 0) {
4033
+ const errorMessage = [
4034
+ "\u274C Configuration validation failed:",
4035
+ "",
4036
+ ...errors.map((err, i) => `${i + 1}. ${err}`),
4037
+ "",
4038
+ "\u{1F4A1} Please check your loly.config.ts file and fix the errors above."
4039
+ ].join("\n");
4040
+ throw new ConfigValidationError(errorMessage, errors);
4041
+ }
4042
+ }
3397
4043
  function loadConfig(projectRoot) {
3398
4044
  const configFiles = [
3399
4045
  import_path18.default.join(projectRoot, "loly.config.ts"),
@@ -3401,6 +4047,7 @@ function loadConfig(projectRoot) {
3401
4047
  import_path18.default.join(projectRoot, "loly.config.json")
3402
4048
  ];
3403
4049
  let userConfig = {};
4050
+ let loadedConfigFile = null;
3404
4051
  for (const configFile of configFiles) {
3405
4052
  if (import_fs16.default.existsSync(configFile)) {
3406
4053
  try {
@@ -3414,16 +4061,31 @@ function loadConfig(projectRoot) {
3414
4061
  const mod = require(configFile);
3415
4062
  userConfig = typeof mod.default === "function" ? mod.default(process.env.NODE_ENV) : mod.default || mod.config || mod;
3416
4063
  }
4064
+ loadedConfigFile = import_path18.default.relative(projectRoot, configFile);
3417
4065
  break;
3418
4066
  } catch (error) {
3419
- console.warn(`[framework] Failed to load config from ${configFile}:`, error);
4067
+ const errorMessage = error instanceof Error ? error.message : String(error);
4068
+ throw new ConfigValidationError(
4069
+ `Failed to load configuration from ${import_path18.default.relative(projectRoot, configFile)}:
4070
+ ${errorMessage}
4071
+ \u{1F4A1} Suggestion: Check that your config file exports a valid configuration object`
4072
+ );
3420
4073
  }
3421
4074
  }
3422
4075
  }
3423
4076
  const config = deepMerge(DEFAULT_CONFIG, userConfig);
3424
- const appDir = import_path18.default.join(projectRoot, config.directories.app);
3425
- if (!import_fs16.default.existsSync(appDir) && process.env.NODE_ENV !== "test") {
3426
- console.warn(`[framework] App directory not found: ${appDir}`);
4077
+ try {
4078
+ validateConfig(config, projectRoot);
4079
+ } catch (error) {
4080
+ if (error instanceof ConfigValidationError) {
4081
+ if (loadedConfigFile) {
4082
+ error.message = `[Configuration Error in ${loadedConfigFile}]
4083
+
4084
+ ${error.message}`;
4085
+ }
4086
+ throw error;
4087
+ }
4088
+ throw error;
3427
4089
  }
3428
4090
  return config;
3429
4091
  }
@@ -3440,14 +4102,14 @@ function getStaticDir(projectRoot, config) {
3440
4102
  // modules/server/setup.ts
3441
4103
  function setupServer(app, options) {
3442
4104
  const { projectRoot, appDir, isDev, config } = options;
3443
- const routeLoader = isDev ? new FilesystemRouteLoader(appDir) : new ManifestRouteLoader(projectRoot);
4105
+ const routeLoader = isDev ? new FilesystemRouteLoader(appDir, projectRoot) : new ManifestRouteLoader(projectRoot);
3444
4106
  if (isDev) {
3445
4107
  let getRoutes2 = function() {
3446
4108
  clearAppRequireCache(appDir);
3447
- const loader = new FilesystemRouteLoader(appDir);
4109
+ sharedLoader.invalidateCache();
3448
4110
  return {
3449
- routes: loader.loadRoutes(),
3450
- apiRoutes: loader.loadApiRoutes()
4111
+ routes: sharedLoader.loadRoutes(),
4112
+ apiRoutes: sharedLoader.loadApiRoutes()
3451
4113
  };
3452
4114
  };
3453
4115
  var getRoutes = getRoutes2;
@@ -3461,19 +4123,20 @@ function setupServer(app, options) {
3461
4123
  console.log(`[hot-reload] Cleared require cache for: ${rel}`);
3462
4124
  }
3463
4125
  if (isPageFile) {
3464
- const loader = new FilesystemRouteLoader(appDir);
4126
+ const loader = new FilesystemRouteLoader(appDir, projectRoot);
3465
4127
  const newRoutes = loader.loadRoutes();
3466
4128
  writeClientRoutesManifest(newRoutes, projectRoot);
3467
4129
  console.log("[hot-reload] Client routes manifest reloaded");
3468
4130
  }
3469
4131
  };
3470
- setupHotReload({ app, appDir, waitForBuild, onFileChange });
4132
+ setupHotReload({ app, appDir, projectRoot, waitForBuild, onFileChange });
3471
4133
  app.use("/static", import_express.default.static(outDir));
3472
4134
  const routes = routeLoader.loadRoutes();
3473
4135
  const wssRoutes = routeLoader.loadWssRoutes();
3474
4136
  const notFoundPage = routeLoader.loadNotFoundRoute();
3475
4137
  const errorPage = routeLoader.loadErrorRoute();
3476
4138
  writeClientRoutesManifest(routes, projectRoot);
4139
+ const sharedLoader = new FilesystemRouteLoader(appDir, projectRoot);
3477
4140
  return {
3478
4141
  routes,
3479
4142
  wssRoutes,
@@ -3561,104 +4224,24 @@ function sanitizeQuery(query) {
3561
4224
 
3562
4225
  // modules/server/middleware/rate-limit.ts
3563
4226
  var import_express_rate_limit = __toESM(require("express-rate-limit"));
3564
- function createRateLimiter(config = {}) {
4227
+
4228
+ // modules/logger/index.ts
4229
+ var import_pino = __toESM(require("pino"));
4230
+ function createLogger(options = {}) {
3565
4231
  const {
3566
- windowMs = 15 * 60 * 1e3,
3567
- // 15 minutes
3568
- max = 100,
3569
- // limit each IP to 100 requests per windowMs
3570
- message = "Too many requests from this IP, please try again later.",
3571
- standardHeaders = true,
3572
- legacyHeaders = false,
3573
- skipSuccessfulRequests = false,
3574
- skipFailedRequests = false
3575
- } = config;
3576
- return (0, import_express_rate_limit.default)({
3577
- windowMs,
3578
- max,
3579
- message: {
3580
- error: message
3581
- },
3582
- standardHeaders,
3583
- legacyHeaders,
3584
- skipSuccessfulRequests,
3585
- skipFailedRequests
3586
- });
3587
- }
3588
- var defaultRateLimiter = createRateLimiter({
3589
- windowMs: 15 * 60 * 1e3,
3590
- // 15 minutes
3591
- max: 100,
3592
- message: "Too many requests from this IP, please try again later."
3593
- });
3594
- var strictRateLimiter = createRateLimiter({
3595
- windowMs: 15 * 60 * 1e3,
3596
- // 15 minutes
3597
- max: 5,
3598
- message: "Too many authentication attempts, please try again later."
3599
- });
3600
- var lenientRateLimiter = createRateLimiter({
3601
- windowMs: 15 * 60 * 1e3,
3602
- // 15 minutes
3603
- max: 200,
3604
- message: "Too many requests from this IP, please try again later."
3605
- });
3606
-
3607
- // modules/server/middleware/auto-rate-limit.ts
3608
- function matchesStrictPattern(path25, patterns) {
3609
- for (const pattern of patterns) {
3610
- const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
3611
- const regex = new RegExp(`^${regexPattern}$`);
3612
- if (regex.test(path25)) {
3613
- return true;
3614
- }
3615
- }
3616
- return false;
3617
- }
3618
- function isRateLimiter(mw) {
3619
- if (!mw) return false;
3620
- if (mw === strictRateLimiter || mw === defaultRateLimiter || mw === lenientRateLimiter) {
3621
- return true;
3622
- }
3623
- if (typeof mw === "function" && mw.name && mw.name.includes("rateLimit")) {
3624
- return true;
3625
- }
3626
- if (mw && typeof mw === "function" && mw.skip || mw.resetKey) {
3627
- return true;
3628
- }
3629
- return false;
3630
- }
3631
- function getAutoRateLimiter(route, strictPatterns = []) {
3632
- const hasRateLimiter = route.middlewares?.some(isRateLimiter) || Object.values(route.methodMiddlewares || {}).some(
3633
- (mws) => mws?.some(isRateLimiter)
3634
- );
3635
- if (hasRateLimiter) {
3636
- return null;
3637
- }
3638
- if (strictPatterns.length > 0 && matchesStrictPattern(route.pattern, strictPatterns)) {
3639
- console.log(`[rate-limit] Applying strict rate limiter to route: ${route.pattern}`);
3640
- return strictRateLimiter;
3641
- }
3642
- return null;
3643
- }
3644
-
3645
- // modules/logger/index.ts
3646
- var import_pino = __toESM(require("pino"));
3647
- function createLogger(options = {}) {
3648
- const {
3649
- level = process.env.LOG_LEVEL || (process.env.NODE_ENV === "development" ? "debug" : "info"),
3650
- enabled = process.env.LOG_ENABLED !== "false",
3651
- pretty = process.env.NODE_ENV === "development",
3652
- destination
3653
- } = options;
3654
- if (!enabled) {
3655
- return (0, import_pino.default)({ enabled: false });
3656
- }
3657
- const baseConfig = {
3658
- level,
3659
- base: {
3660
- name: "@lolyjs/core",
3661
- env: process.env.NODE_ENV || "development"
4232
+ level = process.env.LOG_LEVEL || (process.env.NODE_ENV === "development" ? "debug" : "info"),
4233
+ enabled = process.env.LOG_ENABLED !== "false",
4234
+ pretty = process.env.NODE_ENV === "development",
4235
+ destination
4236
+ } = options;
4237
+ if (!enabled) {
4238
+ return (0, import_pino.default)({ enabled: false });
4239
+ }
4240
+ const baseConfig = {
4241
+ level,
4242
+ base: {
4243
+ name: "@lolyjs/core",
4244
+ env: process.env.NODE_ENV || "development"
3662
4245
  },
3663
4246
  timestamp: import_pino.default.stdTimeFunctions.isoTime,
3664
4247
  formatters: {
@@ -3700,8 +4283,8 @@ function resetLogger() {
3700
4283
  loggerInstance = null;
3701
4284
  }
3702
4285
  var Logger = class _Logger {
3703
- constructor(logger4, context = {}) {
3704
- this.pino = logger4 || getLogger();
4286
+ constructor(logger5, context = {}) {
4287
+ this.pino = logger5 || getLogger();
3705
4288
  this.context = context;
3706
4289
  }
3707
4290
  /**
@@ -3778,12 +4361,12 @@ var DEFAULT_IGNORED_PATHS = [
3778
4361
  /^\/sockjs-node/
3779
4362
  // Hot reload websocket
3780
4363
  ];
3781
- function shouldIgnorePath(path25, ignoredPaths) {
4364
+ function shouldIgnorePath(path28, ignoredPaths) {
3782
4365
  return ignoredPaths.some((pattern) => {
3783
4366
  if (typeof pattern === "string") {
3784
- return path25 === pattern || path25.startsWith(pattern);
4367
+ return path28 === pattern || path28.startsWith(pattern);
3785
4368
  }
3786
- return pattern.test(path25);
4369
+ return pattern.test(path28);
3787
4370
  });
3788
4371
  }
3789
4372
  function requestLoggerMiddleware(options = {}) {
@@ -3834,6 +4417,151 @@ function getRequestLogger(req) {
3834
4417
  return req.logger || logger.child({ requestId: "unknown" });
3835
4418
  }
3836
4419
 
4420
+ // modules/server/middleware/rate-limit.ts
4421
+ var logger2 = createModuleLogger("rate-limit");
4422
+ function validateRateLimitConfig(config) {
4423
+ if (config.windowMs !== void 0 && (config.windowMs < 1e3 || !Number.isInteger(config.windowMs))) {
4424
+ throw new Error(
4425
+ `Invalid rateLimit.windowMs: ${config.windowMs}. Must be an integer >= 1000 (milliseconds)`
4426
+ );
4427
+ }
4428
+ if (config.max !== void 0 && (config.max < 1 || !Number.isInteger(config.max))) {
4429
+ throw new Error(
4430
+ `Invalid rateLimit.max: ${config.max}. Must be an integer >= 1`
4431
+ );
4432
+ }
4433
+ }
4434
+ function createRateLimiter(config = {}) {
4435
+ validateRateLimitConfig(config);
4436
+ const {
4437
+ windowMs = 15 * 60 * 1e3,
4438
+ // 15 minutes
4439
+ max = 100,
4440
+ // limit each IP to 100 requests per windowMs
4441
+ message = "Too many requests from this IP, please try again later.",
4442
+ standardHeaders = true,
4443
+ legacyHeaders = false,
4444
+ skipSuccessfulRequests = false,
4445
+ skipFailedRequests = false,
4446
+ keyGenerator,
4447
+ skip
4448
+ } = config;
4449
+ const limiter = (0, import_express_rate_limit.default)({
4450
+ windowMs,
4451
+ max,
4452
+ message: {
4453
+ error: message,
4454
+ retryAfter: Math.ceil(windowMs / 1e3)
4455
+ // seconds until retry
4456
+ },
4457
+ standardHeaders,
4458
+ legacyHeaders,
4459
+ skipSuccessfulRequests,
4460
+ skipFailedRequests,
4461
+ keyGenerator,
4462
+ skip
4463
+ });
4464
+ const wrappedLimiter = (req, res, next) => {
4465
+ limiter(req, res, (err) => {
4466
+ if (err && res.statusCode === 429) {
4467
+ const ip = req.ip || req.connection?.remoteAddress || "unknown";
4468
+ logger2.warn("Rate limit exceeded", {
4469
+ ip,
4470
+ path: req.path,
4471
+ method: req.method,
4472
+ limit: max,
4473
+ windowMs,
4474
+ retryAfter: Math.ceil(windowMs / 1e3)
4475
+ });
4476
+ }
4477
+ if (err) {
4478
+ return next(err);
4479
+ }
4480
+ next();
4481
+ });
4482
+ };
4483
+ Object.setPrototypeOf(wrappedLimiter, limiter);
4484
+ Object.assign(wrappedLimiter, limiter);
4485
+ return wrappedLimiter;
4486
+ }
4487
+ var defaultRateLimiter = createRateLimiter({
4488
+ windowMs: 15 * 60 * 1e3,
4489
+ // 15 minutes
4490
+ max: 100,
4491
+ message: "Too many requests from this IP, please try again later."
4492
+ });
4493
+ var strictRateLimiter = createRateLimiter({
4494
+ windowMs: 15 * 60 * 1e3,
4495
+ // 15 minutes
4496
+ max: 5,
4497
+ message: "Too many authentication attempts, please try again later."
4498
+ });
4499
+ var lenientRateLimiter = createRateLimiter({
4500
+ windowMs: 15 * 60 * 1e3,
4501
+ // 15 minutes
4502
+ max: 200,
4503
+ message: "Too many requests from this IP, please try again later."
4504
+ });
4505
+ function createRateLimiterFromConfig(config, useApiMax = false) {
4506
+ if (!config) return null;
4507
+ const max = useApiMax ? config.apiMax ?? config.max ?? 100 : config.max ?? 100;
4508
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
4509
+ return createRateLimiter({
4510
+ windowMs,
4511
+ max,
4512
+ message: `Too many requests from this IP, please try again after ${Math.ceil(windowMs / 1e3)} seconds.`
4513
+ });
4514
+ }
4515
+ function createStrictRateLimiterFromConfig(config) {
4516
+ if (!config || config.strictMax === void 0) {
4517
+ return strictRateLimiter;
4518
+ }
4519
+ const windowMs = config.windowMs ?? 15 * 60 * 1e3;
4520
+ return createRateLimiter({
4521
+ windowMs,
4522
+ max: config.strictMax,
4523
+ message: `Too many authentication attempts, please try again after ${Math.ceil(windowMs / 1e3)} seconds.`
4524
+ });
4525
+ }
4526
+
4527
+ // modules/server/middleware/auto-rate-limit.ts
4528
+ function matchesStrictPattern(path28, patterns) {
4529
+ for (const pattern of patterns) {
4530
+ const regexPattern = pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\//g, "\\/");
4531
+ const regex = new RegExp(`^${regexPattern}$`);
4532
+ if (regex.test(path28)) {
4533
+ return true;
4534
+ }
4535
+ }
4536
+ return false;
4537
+ }
4538
+ function isRateLimiter(mw) {
4539
+ if (!mw) return false;
4540
+ if (mw === strictRateLimiter || mw === defaultRateLimiter || mw === lenientRateLimiter) {
4541
+ return true;
4542
+ }
4543
+ if (typeof mw === "function" && mw.name && mw.name.includes("rateLimit")) {
4544
+ return true;
4545
+ }
4546
+ if (mw && typeof mw === "function" && mw.skip || mw.resetKey) {
4547
+ return true;
4548
+ }
4549
+ return false;
4550
+ }
4551
+ function getAutoRateLimiter(route, strictPatterns = [], rateLimitConfig) {
4552
+ const hasRateLimiter = route.middlewares?.some(isRateLimiter) || Object.values(route.methodMiddlewares || {}).some(
4553
+ (mws) => mws?.some(isRateLimiter)
4554
+ );
4555
+ if (hasRateLimiter) {
4556
+ return null;
4557
+ }
4558
+ if (strictPatterns.length > 0 && matchesStrictPattern(route.pattern, strictPatterns)) {
4559
+ const limiter = rateLimitConfig ? createStrictRateLimiterFromConfig(rateLimitConfig) : strictRateLimiter;
4560
+ return limiter;
4561
+ }
4562
+ return null;
4563
+ }
4564
+
3837
4565
  // modules/server/handlers/api.ts
3838
4566
  async function handleApiRequest(options) {
3839
4567
  const { apiRoutes, urlPath, req, res, env = "dev" } = options;
@@ -3865,7 +4593,8 @@ async function handleApiRequest(options) {
3865
4593
  try {
3866
4594
  const autoRateLimiter = getAutoRateLimiter(
3867
4595
  route,
3868
- options.strictRateLimitPatterns
4596
+ options.strictRateLimitPatterns,
4597
+ options.rateLimitConfig
3869
4598
  );
3870
4599
  const reqLogger = getRequestLogger(req);
3871
4600
  if (autoRateLimiter) {
@@ -3877,26 +4606,46 @@ async function handleApiRequest(options) {
3877
4606
  const globalMws = route.middlewares ?? [];
3878
4607
  const perMethodMws = route.methodMiddlewares?.[method] ?? [];
3879
4608
  const chain = autoRateLimiter ? [autoRateLimiter, ...globalMws, ...perMethodMws] : [...globalMws, ...perMethodMws];
3880
- for (const mw of chain) {
3881
- const isExpressRateLimit = mw && typeof mw === "function" && (mw.skip || mw.resetKey || mw.name?.includes("rateLimit"));
3882
- if (isExpressRateLimit) {
3883
- await new Promise((resolve3, reject) => {
3884
- const next = (err) => {
3885
- if (err) reject(err);
3886
- else resolve3();
3887
- };
3888
- try {
3889
- const result = mw(req, res, next);
3890
- if (result && typeof result.then === "function") {
3891
- result.then(() => resolve3()).catch(reject);
4609
+ for (let i = 0; i < chain.length; i++) {
4610
+ const mw = chain[i];
4611
+ if (typeof mw !== "function") {
4612
+ reqLogger.warn("Invalid middleware in chain", {
4613
+ route: route.pattern,
4614
+ method,
4615
+ middlewareIndex: i,
4616
+ middlewareType: typeof mw
4617
+ });
4618
+ continue;
4619
+ }
4620
+ try {
4621
+ const isExpressRateLimit = mw.skip || mw.resetKey || mw.name?.includes("rateLimit");
4622
+ if (isExpressRateLimit) {
4623
+ await new Promise((resolve3, reject) => {
4624
+ const next = (err) => {
4625
+ if (err) reject(err);
4626
+ else resolve3();
4627
+ };
4628
+ try {
4629
+ const result = mw(req, res, next);
4630
+ if (result && typeof result.then === "function") {
4631
+ result.then(() => resolve3()).catch(reject);
4632
+ }
4633
+ } catch (err) {
4634
+ reject(err);
3892
4635
  }
3893
- } catch (err) {
3894
- reject(err);
3895
- }
4636
+ });
4637
+ } else {
4638
+ await Promise.resolve(mw(ctx, async () => {
4639
+ }));
4640
+ }
4641
+ } catch (error) {
4642
+ reqLogger.error("API middleware failed", error instanceof Error ? error : new Error(String(error)), {
4643
+ route: route.pattern,
4644
+ method,
4645
+ middlewareIndex: i,
4646
+ middlewareName: mw.name || "anonymous"
3896
4647
  });
3897
- } else {
3898
- await Promise.resolve(mw(ctx, async () => {
3899
- }));
4648
+ throw error;
3900
4649
  }
3901
4650
  if (res.headersSent) {
3902
4651
  return;
@@ -3947,42 +4696,289 @@ function createDocumentTree(options) {
3947
4696
  titleFallback,
3948
4697
  descriptionFallback,
3949
4698
  chunkHref,
4699
+ entrypointFiles = [],
3950
4700
  theme,
3951
4701
  clientJsPath = "/static/client.js",
3952
4702
  clientCssPath = "/static/client.css",
3953
- nonce
4703
+ nonce,
4704
+ includeInlineScripts = true
4705
+ // Default true - scripts inline in body for both SSR and SSG
3954
4706
  } = options;
3955
- const metaObj = meta ?? {};
3956
- const title = metaObj.title ?? titleFallback ?? "My Framework Dev";
3957
- const lang = metaObj.lang ?? "en";
3958
- const description = metaObj.description ?? descriptionFallback ?? "Demo Loly framework";
4707
+ const metaObj = meta ?? null;
4708
+ const title = metaObj?.title ?? titleFallback ?? "My Framework Dev";
4709
+ const lang = metaObj?.lang ?? "en";
4710
+ const description = metaObj?.description ?? descriptionFallback ?? "Demo Loly framework";
3959
4711
  const extraMetaTags = [];
4712
+ const linkTags = [];
3960
4713
  if (description) {
3961
4714
  extraMetaTags.push(
3962
4715
  import_react.default.createElement("meta", {
4716
+ key: "meta-description",
3963
4717
  name: "description",
3964
4718
  content: description
3965
4719
  })
3966
4720
  );
3967
4721
  }
3968
- if (Array.isArray(metaObj.metaTags)) {
3969
- for (const tag of metaObj.metaTags) {
4722
+ if (metaObj?.robots) {
4723
+ extraMetaTags.push(
4724
+ import_react.default.createElement("meta", {
4725
+ key: "meta-robots",
4726
+ name: "robots",
4727
+ content: metaObj.robots
4728
+ })
4729
+ );
4730
+ }
4731
+ if (metaObj?.themeColor) {
4732
+ extraMetaTags.push(
4733
+ import_react.default.createElement("meta", {
4734
+ key: "meta-theme-color",
4735
+ name: "theme-color",
4736
+ content: metaObj.themeColor
4737
+ })
4738
+ );
4739
+ }
4740
+ if (metaObj?.viewport) {
4741
+ extraMetaTags.push(
4742
+ import_react.default.createElement("meta", {
4743
+ key: "meta-viewport",
4744
+ name: "viewport",
4745
+ content: metaObj.viewport
4746
+ })
4747
+ );
4748
+ }
4749
+ if (metaObj?.canonical) {
4750
+ linkTags.push(
4751
+ import_react.default.createElement("link", {
4752
+ key: "link-canonical",
4753
+ rel: "canonical",
4754
+ href: metaObj.canonical
4755
+ })
4756
+ );
4757
+ }
4758
+ if (metaObj?.openGraph) {
4759
+ const og = metaObj.openGraph;
4760
+ if (og.title) {
4761
+ extraMetaTags.push(
4762
+ import_react.default.createElement("meta", {
4763
+ key: "og-title",
4764
+ property: "og:title",
4765
+ content: og.title
4766
+ })
4767
+ );
4768
+ }
4769
+ if (og.description) {
4770
+ extraMetaTags.push(
4771
+ import_react.default.createElement("meta", {
4772
+ key: "og-description",
4773
+ property: "og:description",
4774
+ content: og.description
4775
+ })
4776
+ );
4777
+ }
4778
+ if (og.type) {
3970
4779
  extraMetaTags.push(
3971
4780
  import_react.default.createElement("meta", {
4781
+ key: "og-type",
4782
+ property: "og:type",
4783
+ content: og.type
4784
+ })
4785
+ );
4786
+ }
4787
+ if (og.url) {
4788
+ extraMetaTags.push(
4789
+ import_react.default.createElement("meta", {
4790
+ key: "og-url",
4791
+ property: "og:url",
4792
+ content: og.url
4793
+ })
4794
+ );
4795
+ }
4796
+ if (og.image) {
4797
+ if (typeof og.image === "string") {
4798
+ extraMetaTags.push(
4799
+ import_react.default.createElement("meta", {
4800
+ key: "og-image",
4801
+ property: "og:image",
4802
+ content: og.image
4803
+ })
4804
+ );
4805
+ } else {
4806
+ extraMetaTags.push(
4807
+ import_react.default.createElement("meta", {
4808
+ key: "og-image",
4809
+ property: "og:image",
4810
+ content: og.image.url
4811
+ })
4812
+ );
4813
+ if (og.image.width) {
4814
+ extraMetaTags.push(
4815
+ import_react.default.createElement("meta", {
4816
+ key: "og-image-width",
4817
+ property: "og:image:width",
4818
+ content: String(og.image.width)
4819
+ })
4820
+ );
4821
+ }
4822
+ if (og.image.height) {
4823
+ extraMetaTags.push(
4824
+ import_react.default.createElement("meta", {
4825
+ key: "og-image-height",
4826
+ property: "og:image:height",
4827
+ content: String(og.image.height)
4828
+ })
4829
+ );
4830
+ }
4831
+ if (og.image.alt) {
4832
+ extraMetaTags.push(
4833
+ import_react.default.createElement("meta", {
4834
+ key: "og-image-alt",
4835
+ property: "og:image:alt",
4836
+ content: og.image.alt
4837
+ })
4838
+ );
4839
+ }
4840
+ }
4841
+ }
4842
+ if (og.siteName) {
4843
+ extraMetaTags.push(
4844
+ import_react.default.createElement("meta", {
4845
+ key: "og-site-name",
4846
+ property: "og:site_name",
4847
+ content: og.siteName
4848
+ })
4849
+ );
4850
+ }
4851
+ if (og.locale) {
4852
+ extraMetaTags.push(
4853
+ import_react.default.createElement("meta", {
4854
+ key: "og-locale",
4855
+ property: "og:locale",
4856
+ content: og.locale
4857
+ })
4858
+ );
4859
+ }
4860
+ }
4861
+ if (metaObj?.twitter) {
4862
+ const twitter = metaObj.twitter;
4863
+ if (twitter.card) {
4864
+ extraMetaTags.push(
4865
+ import_react.default.createElement("meta", {
4866
+ key: "twitter-card",
4867
+ name: "twitter:card",
4868
+ content: twitter.card
4869
+ })
4870
+ );
4871
+ }
4872
+ if (twitter.title) {
4873
+ extraMetaTags.push(
4874
+ import_react.default.createElement("meta", {
4875
+ key: "twitter-title",
4876
+ name: "twitter:title",
4877
+ content: twitter.title
4878
+ })
4879
+ );
4880
+ }
4881
+ if (twitter.description) {
4882
+ extraMetaTags.push(
4883
+ import_react.default.createElement("meta", {
4884
+ key: "twitter-description",
4885
+ name: "twitter:description",
4886
+ content: twitter.description
4887
+ })
4888
+ );
4889
+ }
4890
+ if (twitter.image) {
4891
+ extraMetaTags.push(
4892
+ import_react.default.createElement("meta", {
4893
+ key: "twitter-image",
4894
+ name: "twitter:image",
4895
+ content: twitter.image
4896
+ })
4897
+ );
4898
+ }
4899
+ if (twitter.imageAlt) {
4900
+ extraMetaTags.push(
4901
+ import_react.default.createElement("meta", {
4902
+ key: "twitter-image-alt",
4903
+ name: "twitter:image:alt",
4904
+ content: twitter.imageAlt
4905
+ })
4906
+ );
4907
+ }
4908
+ if (twitter.site) {
4909
+ extraMetaTags.push(
4910
+ import_react.default.createElement("meta", {
4911
+ key: "twitter-site",
4912
+ name: "twitter:site",
4913
+ content: twitter.site
4914
+ })
4915
+ );
4916
+ }
4917
+ if (twitter.creator) {
4918
+ extraMetaTags.push(
4919
+ import_react.default.createElement("meta", {
4920
+ key: "twitter-creator",
4921
+ name: "twitter:creator",
4922
+ content: twitter.creator
4923
+ })
4924
+ );
4925
+ }
4926
+ }
4927
+ if (metaObj?.metaTags && Array.isArray(metaObj.metaTags)) {
4928
+ metaObj.metaTags.forEach((tag, index) => {
4929
+ extraMetaTags.push(
4930
+ import_react.default.createElement("meta", {
4931
+ key: `meta-custom-${index}`,
3972
4932
  name: tag.name,
3973
4933
  property: tag.property,
4934
+ httpEquiv: tag.httpEquiv,
3974
4935
  content: tag.content
3975
4936
  })
3976
4937
  );
3977
- }
4938
+ });
4939
+ }
4940
+ if (metaObj?.links && Array.isArray(metaObj.links)) {
4941
+ metaObj.links.forEach((link, index) => {
4942
+ linkTags.push(
4943
+ import_react.default.createElement("link", {
4944
+ key: `link-custom-${index}`,
4945
+ rel: link.rel,
4946
+ href: link.href,
4947
+ as: link.as,
4948
+ crossOrigin: link.crossorigin,
4949
+ type: link.type
4950
+ })
4951
+ );
4952
+ });
4953
+ }
4954
+ const serialized = JSON.stringify({
4955
+ ...initialData,
4956
+ theme
4957
+ });
4958
+ const routerSerialized = JSON.stringify({
4959
+ ...routerData
4960
+ });
4961
+ const bodyChildren = [
4962
+ import_react.default.createElement("div", { id: APP_CONTAINER_ID }, appTree)
4963
+ ];
4964
+ if (includeInlineScripts) {
4965
+ bodyChildren.push(
4966
+ import_react.default.createElement("script", {
4967
+ key: "initial-data",
4968
+ nonce,
4969
+ dangerouslySetInnerHTML: {
4970
+ __html: `window.${WINDOW_DATA_KEY} = ${serialized};`
4971
+ }
4972
+ }),
4973
+ import_react.default.createElement("script", {
4974
+ key: "router-data",
4975
+ nonce,
4976
+ dangerouslySetInnerHTML: {
4977
+ __html: `window.${ROUTER_DATA_KEY} = ${routerSerialized};`
4978
+ }
4979
+ })
4980
+ );
3978
4981
  }
3979
- const serialized = JSON.stringify({
3980
- ...initialData,
3981
- theme
3982
- });
3983
- const routerSerialized = JSON.stringify({
3984
- ...routerData
3985
- });
3986
4982
  const documentTree = import_react.default.createElement(
3987
4983
  "html",
3988
4984
  { lang },
@@ -3991,13 +4987,25 @@ function createDocumentTree(options) {
3991
4987
  null,
3992
4988
  import_react.default.createElement("meta", { charSet: "utf-8" }),
3993
4989
  import_react.default.createElement("title", null, title),
4990
+ // Viewport: use custom if provided, otherwise default
3994
4991
  import_react.default.createElement("meta", {
3995
4992
  name: "viewport",
3996
- content: "width=device-width, initial-scale=1"
4993
+ content: metaObj?.viewport ?? "width=device-width, initial-scale=1"
3997
4994
  }),
3998
4995
  ...extraMetaTags,
4996
+ ...linkTags,
4997
+ ...entrypointFiles.length > 0 ? entrypointFiles.slice(0, -1).map(
4998
+ (file) => import_react.default.createElement("link", {
4999
+ key: `preload-${file}`,
5000
+ rel: "preload",
5001
+ href: file,
5002
+ as: "script"
5003
+ })
5004
+ ) : [],
5005
+ // Preload route-specific chunk if available
3999
5006
  chunkHref && import_react.default.createElement("link", {
4000
- rel: "modulepreload",
5007
+ key: `preload-${chunkHref}`,
5008
+ rel: "preload",
4001
5009
  href: chunkHref,
4002
5010
  as: "script"
4003
5011
  }),
@@ -4010,33 +5018,34 @@ function createDocumentTree(options) {
4010
5018
  rel: "stylesheet",
4011
5019
  href: clientCssPath
4012
5020
  }),
4013
- import_react.default.createElement("script", {
4014
- src: clientJsPath,
4015
- defer: true
4016
- })
5021
+ ...entrypointFiles.length > 0 ? entrypointFiles.map(
5022
+ (file) => import_react.default.createElement("script", {
5023
+ key: file,
5024
+ src: file,
5025
+ defer: true,
5026
+ nonce
5027
+ // CSP nonce for external scripts
5028
+ })
5029
+ ) : [
5030
+ import_react.default.createElement("script", {
5031
+ key: "client",
5032
+ src: clientJsPath,
5033
+ defer: true,
5034
+ nonce
5035
+ // CSP nonce for external scripts
5036
+ })
5037
+ ]
4017
5038
  ),
4018
5039
  import_react.default.createElement(
4019
5040
  "body",
4020
5041
  {
4021
5042
  style: { margin: 0 },
4022
- className: [initialData.className, theme].filter(Boolean).join(" "),
5043
+ className: [initialData.className || "", theme].filter(Boolean).join(" "),
4023
5044
  suppressHydrationWarning: true
4024
5045
  // Allow theme class to differ between server and client initially
4025
5046
  },
4026
- import_react.default.createElement("div", { id: APP_CONTAINER_ID }, appTree)
4027
- ),
4028
- import_react.default.createElement("script", {
4029
- nonce,
4030
- dangerouslySetInnerHTML: {
4031
- __html: `window.${WINDOW_DATA_KEY} = ${serialized};`
4032
- }
4033
- }),
4034
- import_react.default.createElement("script", {
4035
- nonce,
4036
- dangerouslySetInnerHTML: {
4037
- __html: `window.${ROUTER_DATA_KEY} = ${routerSerialized};`
4038
- }
4039
- })
5047
+ ...bodyChildren
5048
+ )
4040
5049
  );
4041
5050
  return documentTree;
4042
5051
  }
@@ -4052,7 +5061,7 @@ function buildInitialData(urlPath, params, loaderResult) {
4052
5061
  params,
4053
5062
  props,
4054
5063
  metadata: loaderResult.metadata ?? null,
4055
- className: loaderResult.className ?? "",
5064
+ className: loaderResult.className,
4056
5065
  error: false,
4057
5066
  notFound: false
4058
5067
  };
@@ -4068,12 +5077,25 @@ var buildRouterData = (req) => {
4068
5077
  };
4069
5078
 
4070
5079
  // modules/server/handlers/middleware.ts
5080
+ var import_path20 = __toESM(require("path"));
4071
5081
  async function runRouteMiddlewares(route, ctx) {
4072
- for (const mw of route.middlewares) {
4073
- await Promise.resolve(
4074
- mw(ctx, async () => {
4075
- })
4076
- );
5082
+ for (let i = 0; i < route.middlewares.length; i++) {
5083
+ const mw = route.middlewares[i];
5084
+ try {
5085
+ await Promise.resolve(
5086
+ mw(ctx, async () => {
5087
+ })
5088
+ );
5089
+ } catch (error) {
5090
+ const reqLogger = getRequestLogger(ctx.req);
5091
+ const relativePath = route.pageFile ? import_path20.default.relative(process.cwd(), route.pageFile) : route.pattern;
5092
+ reqLogger.error("Route middleware failed", error instanceof Error ? error : new Error(String(error)), {
5093
+ route: route.pattern,
5094
+ middlewareIndex: i,
5095
+ pageFile: relativePath
5096
+ });
5097
+ throw error;
5098
+ }
4077
5099
  if (ctx.res.headersSent) {
4078
5100
  return;
4079
5101
  }
@@ -4081,11 +5103,57 @@ async function runRouteMiddlewares(route, ctx) {
4081
5103
  }
4082
5104
 
4083
5105
  // modules/server/handlers/server-hook.ts
5106
+ var import_path21 = __toESM(require("path"));
5107
+ function createServerHookErrorMessage(error, hookType, routePattern, filePath) {
5108
+ const errorMessage = error instanceof Error ? error.message : String(error);
5109
+ const errorStack = error instanceof Error ? error.stack : void 0;
5110
+ let message = `[${hookType.toUpperCase()} SERVER HOOK ERROR]
5111
+ `;
5112
+ message += `Route: ${routePattern}
5113
+ `;
5114
+ if (filePath) {
5115
+ const relativePath = import_path21.default.relative(process.cwd(), filePath);
5116
+ message += `File: ${relativePath}
5117
+ `;
5118
+ }
5119
+ message += `Error: ${errorMessage}
5120
+ `;
5121
+ if (errorMessage.includes("Cannot find module")) {
5122
+ message += `
5123
+ \u{1F4A1} Suggestion: Check that all imports in your ${hookType}.server.hook.ts are correct.
5124
+ `;
5125
+ } else if (errorMessage.includes("is not defined") || errorMessage.includes("Cannot read property")) {
5126
+ message += `
5127
+ \u{1F4A1} Suggestion: Verify that all variables and properties exist in your ${hookType}.server.hook.ts.
5128
+ `;
5129
+ } else if (errorMessage.includes("async") || errorMessage.includes("await")) {
5130
+ message += `
5131
+ \u{1F4A1} Suggestion: Make sure getServerSideProps is an async function and all promises are awaited.
5132
+ `;
5133
+ }
5134
+ if (errorStack) {
5135
+ message += `
5136
+ Stack trace:
5137
+ ${errorStack}`;
5138
+ }
5139
+ return message;
5140
+ }
4084
5141
  async function runRouteServerHook(route, ctx) {
4085
5142
  if (!route.loader) {
4086
5143
  return { props: {} };
4087
5144
  }
4088
- return await route.loader(ctx);
5145
+ try {
5146
+ return await route.loader(ctx);
5147
+ } catch (error) {
5148
+ const detailedError = new Error(
5149
+ createServerHookErrorMessage(error, "page", route.pattern, route.pageFile)
5150
+ );
5151
+ if (error instanceof Error && error.stack) {
5152
+ detailedError.stack = error.stack;
5153
+ }
5154
+ detailedError.originalError = error;
5155
+ throw detailedError;
5156
+ }
4089
5157
  }
4090
5158
 
4091
5159
  // modules/server/handlers/response.ts
@@ -4126,26 +5194,26 @@ function handleNotFound(res, urlPath) {
4126
5194
 
4127
5195
  // modules/server/handlers/ssg.ts
4128
5196
  var import_fs17 = __toESM(require("fs"));
4129
- var import_path20 = __toESM(require("path"));
4130
- var logger2 = createModuleLogger("ssg");
5197
+ var import_path22 = __toESM(require("path"));
5198
+ var logger3 = createModuleLogger("ssg");
4131
5199
  function getSsgDirForPath(baseDir, urlPath) {
4132
5200
  const clean = urlPath === "/" ? "" : urlPath.replace(/^\/+/, "");
4133
- return import_path20.default.join(baseDir, clean);
5201
+ return import_path22.default.join(baseDir, clean);
4134
5202
  }
4135
5203
  function getSsgHtmlPath(baseDir, urlPath) {
4136
5204
  const dir = getSsgDirForPath(baseDir, urlPath);
4137
- return import_path20.default.join(dir, "index.html");
5205
+ return import_path22.default.join(dir, "index.html");
4138
5206
  }
4139
5207
  function getSsgDataPath(baseDir, urlPath) {
4140
5208
  const dir = getSsgDirForPath(baseDir, urlPath);
4141
- return import_path20.default.join(dir, "data.json");
5209
+ return import_path22.default.join(dir, "data.json");
4142
5210
  }
4143
5211
  function tryServeSsgHtml(res, ssgOutDir, urlPath) {
4144
5212
  const ssgHtmlPath = getSsgHtmlPath(ssgOutDir, urlPath);
4145
5213
  if (!import_fs17.default.existsSync(ssgHtmlPath)) {
4146
5214
  return false;
4147
5215
  }
4148
- logger2.info("Serving SSG HTML", { urlPath, ssgHtmlPath });
5216
+ logger3.info("Serving SSG HTML", { urlPath, ssgHtmlPath });
4149
5217
  res.setHeader(
4150
5218
  "Content-Security-Policy",
4151
5219
  "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; font-src 'self' data: https://fonts.gstatic.com; object-src 'none'; media-src 'self' https:; frame-src 'none';"
@@ -4167,13 +5235,43 @@ function tryServeSsgData(res, ssgOutDir, urlPath) {
4167
5235
  res.status(200).end(raw);
4168
5236
  return true;
4169
5237
  } catch (err) {
4170
- logger2.error("Error reading SSG data", err, { urlPath, ssgDataPath });
5238
+ logger3.error("Error reading SSG data", err, { urlPath, ssgDataPath });
4171
5239
  return false;
4172
5240
  }
4173
5241
  }
4174
5242
 
4175
5243
  // modules/server/handlers/pages.ts
4176
5244
  init_globals();
5245
+ var import_path23 = __toESM(require("path"));
5246
+ function mergeMetadata(base, override) {
5247
+ if (!base && !override) return null;
5248
+ if (!base) return override;
5249
+ if (!override) return base;
5250
+ return {
5251
+ // Simple fields: override wins
5252
+ title: override.title ?? base.title,
5253
+ description: override.description ?? base.description,
5254
+ lang: override.lang ?? base.lang,
5255
+ canonical: override.canonical ?? base.canonical,
5256
+ robots: override.robots ?? base.robots,
5257
+ themeColor: override.themeColor ?? base.themeColor,
5258
+ viewport: override.viewport ?? base.viewport,
5259
+ // Nested objects: shallow merge (override wins for each field)
5260
+ openGraph: override.openGraph ? {
5261
+ ...base.openGraph,
5262
+ ...override.openGraph,
5263
+ // For image, if override has image, use it entirely (don't merge)
5264
+ image: override.openGraph.image ?? base.openGraph?.image
5265
+ } : base.openGraph,
5266
+ twitter: override.twitter ? {
5267
+ ...base.twitter,
5268
+ ...override.twitter
5269
+ } : base.twitter,
5270
+ // Arrays: override replaces base entirely (not merged)
5271
+ metaTags: override.metaTags ?? base.metaTags,
5272
+ links: override.links ?? base.links
5273
+ };
5274
+ }
4177
5275
  function isDataRequest(req) {
4178
5276
  return req.query && req.query.__fw_data === "1" || req.headers["x-fw-data"] === "1";
4179
5277
  }
@@ -4250,9 +5348,13 @@ async function handlePageRequestInternal(options) {
4250
5348
  }
4251
5349
  } catch (error) {
4252
5350
  const reqLogger2 = getRequestLogger(req);
4253
- reqLogger2.warn(`Layout server hook ${i} failed for not-found`, {
4254
- error,
4255
- layoutFile: notFoundPage.layoutFiles[i]
5351
+ const layoutFile = notFoundPage.layoutFiles[i];
5352
+ const relativeLayoutPath = layoutFile ? import_path23.default.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5353
+ reqLogger2.warn("Layout server hook failed for not-found page", {
5354
+ error: error instanceof Error ? error.message : String(error),
5355
+ stack: error instanceof Error ? error.stack : void 0,
5356
+ layoutFile: relativeLayoutPath,
5357
+ layoutIndex: i
4256
5358
  });
4257
5359
  }
4258
5360
  }
@@ -4274,6 +5376,10 @@ async function handlePageRequestInternal(options) {
4274
5376
  const appTree2 = buildAppTree(notFoundPage, {}, initialData2.props);
4275
5377
  initialData2.notFound = true;
4276
5378
  const nonce2 = res.locals.nonce || void 0;
5379
+ const entrypointFiles2 = [];
5380
+ if (assetManifest?.entrypoints?.client) {
5381
+ entrypointFiles2.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5382
+ }
4277
5383
  const documentTree2 = createDocumentTree({
4278
5384
  appTree: appTree2,
4279
5385
  initialData: initialData2,
@@ -4282,6 +5388,7 @@ async function handlePageRequestInternal(options) {
4282
5388
  titleFallback: "Not found",
4283
5389
  descriptionFallback: "Loly demo",
4284
5390
  chunkHref: null,
5391
+ entrypointFiles: entrypointFiles2,
4285
5392
  theme,
4286
5393
  clientJsPath,
4287
5394
  clientCssPath,
@@ -4334,6 +5441,7 @@ async function handlePageRequestInternal(options) {
4334
5441
  return;
4335
5442
  }
4336
5443
  const layoutProps = {};
5444
+ const layoutMetadata = [];
4337
5445
  const reqLogger = getRequestLogger(req);
4338
5446
  if (route.layoutServerHooks && route.layoutServerHooks.length > 0) {
4339
5447
  for (let i = 0; i < route.layoutServerHooks.length; i++) {
@@ -4344,11 +5452,19 @@ async function handlePageRequestInternal(options) {
4344
5452
  if (layoutResult.props) {
4345
5453
  Object.assign(layoutProps, layoutResult.props);
4346
5454
  }
5455
+ if (layoutResult.metadata) {
5456
+ layoutMetadata.push(layoutResult.metadata);
5457
+ }
4347
5458
  } catch (error) {
4348
- reqLogger.warn(`Layout server hook ${i} failed`, {
4349
- error,
4350
- layoutFile: route.layoutFiles[i],
4351
- route: route.pattern
5459
+ const layoutFile = route.layoutFiles[i];
5460
+ const relativeLayoutPath = layoutFile ? import_path23.default.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5461
+ reqLogger.warn("Layout server hook failed", {
5462
+ error: error instanceof Error ? error.message : String(error),
5463
+ stack: error instanceof Error ? error.stack : void 0,
5464
+ layoutFile: relativeLayoutPath,
5465
+ route: route.pattern,
5466
+ layoutIndex: i,
5467
+ suggestion: "Check your layout.server.hook.ts file for errors"
4352
5468
  });
4353
5469
  }
4354
5470
  }
@@ -4361,10 +5477,25 @@ async function handlePageRequestInternal(options) {
4361
5477
  loaderResult.theme = theme;
4362
5478
  }
4363
5479
  } catch (error) {
5480
+ const relativePagePath = route.pageFile ? import_path23.default.relative(projectRoot || process.cwd(), route.pageFile) : "unknown";
5481
+ reqLogger.error("Page server hook failed", {
5482
+ error: error instanceof Error ? error.message : String(error),
5483
+ stack: error instanceof Error ? error.stack : void 0,
5484
+ pageFile: relativePagePath,
5485
+ route: route.pattern,
5486
+ pathname: urlPath,
5487
+ suggestion: "Check your page.server.hook.ts (or server.hook.ts) file for errors"
5488
+ });
4364
5489
  if (isDataReq) {
4365
5490
  res.statusCode = 500;
4366
5491
  res.setHeader("Content-Type", "application/json; charset=utf-8");
4367
- res.end(JSON.stringify({ error: true, message: String(error) }));
5492
+ const errorResponse = {
5493
+ error: true,
5494
+ message: error instanceof Error ? error.message : String(error),
5495
+ route: route.pattern,
5496
+ pageFile: relativePagePath
5497
+ };
5498
+ res.end(JSON.stringify(errorResponse, null, 2));
4368
5499
  return;
4369
5500
  } else {
4370
5501
  if (errorPage) {
@@ -4381,9 +5512,19 @@ async function handlePageRequestInternal(options) {
4381
5512
  ...loaderResult.props || {}
4382
5513
  // Props from page (overrides layout)
4383
5514
  };
5515
+ let combinedMetadata = null;
5516
+ for (const layoutMeta of layoutMetadata) {
5517
+ if (layoutMeta) {
5518
+ combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
5519
+ }
5520
+ }
5521
+ if (loaderResult.metadata) {
5522
+ combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
5523
+ }
4384
5524
  const combinedLoaderResult = {
4385
5525
  ...loaderResult,
4386
- props: combinedProps
5526
+ props: combinedProps,
5527
+ metadata: combinedMetadata
4387
5528
  };
4388
5529
  if (isDataReq) {
4389
5530
  handleDataResponse(res, combinedLoaderResult, theme);
@@ -4413,6 +5554,10 @@ async function handlePageRequestInternal(options) {
4413
5554
  }
4414
5555
  }
4415
5556
  const nonce = res.locals.nonce || void 0;
5557
+ const entrypointFiles = [];
5558
+ if (assetManifest?.entrypoints?.client) {
5559
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5560
+ }
4416
5561
  const documentTree = createDocumentTree({
4417
5562
  appTree,
4418
5563
  initialData,
@@ -4421,6 +5566,7 @@ async function handlePageRequestInternal(options) {
4421
5566
  titleFallback: "Loly framework",
4422
5567
  descriptionFallback: "Loly demo",
4423
5568
  chunkHref,
5569
+ entrypointFiles,
4424
5570
  theme,
4425
5571
  clientJsPath,
4426
5572
  clientCssPath,
@@ -4439,7 +5585,16 @@ async function handlePageRequestInternal(options) {
4439
5585
  onShellError(err) {
4440
5586
  didError = true;
4441
5587
  const reqLogger2 = getRequestLogger(req);
4442
- reqLogger2.error("SSR shell error", err, { route: matched?.route?.pattern || "unknown" });
5588
+ const routePattern = matched?.route?.pattern || "unknown";
5589
+ reqLogger2.error("SSR shell error", err, { route: routePattern });
5590
+ const errorMessage = err instanceof Error ? err.message : String(err);
5591
+ console.error(`
5592
+ \u274C [framework][ssr] Shell error for route "${routePattern}":`);
5593
+ console.error(` ${errorMessage}`);
5594
+ if (err instanceof Error && err.stack) {
5595
+ console.error(` Stack: ${err.stack.split("\n").slice(0, 3).join("\n ")}`);
5596
+ }
5597
+ console.error("\u{1F4A1} This usually indicates a React rendering error\n");
4443
5598
  if (!res.headersSent && errorPage) {
4444
5599
  renderErrorPageWithStream(errorPage, req, res, err, routeChunks, theme, projectRoot, env);
4445
5600
  } else if (!res.headersSent) {
@@ -4452,7 +5607,11 @@ async function handlePageRequestInternal(options) {
4452
5607
  onError(err) {
4453
5608
  didError = true;
4454
5609
  const reqLogger2 = getRequestLogger(req);
4455
- reqLogger2.error("SSR error", err, { route: matched?.route?.pattern || "unknown" });
5610
+ const routePattern = matched?.route?.pattern || "unknown";
5611
+ reqLogger2.error("SSR error", err, { route: routePattern });
5612
+ const errorMessage = err instanceof Error ? err.message : String(err);
5613
+ console.error(`\u26A0\uFE0F [framework][ssr] Error during streaming for route "${routePattern}":`);
5614
+ console.error(` ${errorMessage}`);
4456
5615
  }
4457
5616
  });
4458
5617
  req.on("close", () => {
@@ -4481,9 +5640,13 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4481
5640
  Object.assign(layoutProps, layoutResult.props);
4482
5641
  }
4483
5642
  } catch (err) {
4484
- reqLogger.warn(`Layout server hook ${i} failed for error page`, {
4485
- error: err,
4486
- layoutFile: errorPage.layoutFiles[i]
5643
+ const layoutFile = errorPage.layoutFiles[i];
5644
+ const relativeLayoutPath = layoutFile ? import_path23.default.relative(projectRoot || process.cwd(), layoutFile) : "unknown";
5645
+ reqLogger.warn("Layout server hook failed for error page", {
5646
+ error: err instanceof Error ? err.message : String(err),
5647
+ stack: err instanceof Error ? err.stack : void 0,
5648
+ layoutFile: relativeLayoutPath,
5649
+ layoutIndex: i
4487
5650
  });
4488
5651
  }
4489
5652
  }
@@ -4530,6 +5693,10 @@ async function renderErrorPageWithStream(errorPage, req, res, error, routeChunks
4530
5693
  }
4531
5694
  }
4532
5695
  const nonce = res.locals.nonce || void 0;
5696
+ const entrypointFiles = [];
5697
+ if (assetManifest?.entrypoints?.client) {
5698
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
5699
+ }
4533
5700
  const documentTree = createDocumentTree({
4534
5701
  appTree,
4535
5702
  initialData,
@@ -4655,7 +5822,7 @@ async function getServerConfig(projectRoot) {
4655
5822
  }
4656
5823
 
4657
5824
  // modules/server/routes.ts
4658
- var import_path21 = __toESM(require("path"));
5825
+ var import_path24 = __toESM(require("path"));
4659
5826
  function setupRoutes(options) {
4660
5827
  const {
4661
5828
  app,
@@ -4670,21 +5837,23 @@ function setupRoutes(options) {
4670
5837
  config
4671
5838
  } = options;
4672
5839
  const routeChunks = routeLoader.loadRouteChunks();
4673
- const ssgOutDir = import_path21.default.join(
4674
- config ? getBuildDir(projectRoot, config) : import_path21.default.join(projectRoot, BUILD_FOLDER_NAME),
5840
+ const ssgOutDir = import_path24.default.join(
5841
+ config ? getBuildDir(projectRoot, config) : import_path24.default.join(projectRoot, BUILD_FOLDER_NAME),
4675
5842
  "ssg"
4676
5843
  );
4677
5844
  app.all("/api/*", async (req, res) => {
4678
5845
  const apiRoutes = isDev && getRoutes ? getRoutes().apiRoutes : initialApiRoutes;
4679
5846
  const serverConfig = await getServerConfig(projectRoot);
4680
5847
  const strictPatterns = serverConfig.rateLimit?.strictPatterns || [];
5848
+ const rateLimitConfig = serverConfig.rateLimit;
4681
5849
  await handleApiRequest({
4682
5850
  apiRoutes,
4683
5851
  urlPath: req.path,
4684
5852
  req,
4685
5853
  res,
4686
5854
  env: isDev ? "dev" : "prod",
4687
- strictRateLimitPatterns: strictPatterns
5855
+ strictRateLimitPatterns: strictPatterns,
5856
+ rateLimitConfig
4688
5857
  });
4689
5858
  });
4690
5859
  app.get("*", async (req, res) => {
@@ -4937,12 +6106,29 @@ var setupApplication = async ({
4937
6106
  corsOptions.origin = process.env.NODE_ENV === "development";
4938
6107
  }
4939
6108
  app.use((0, import_cors.default)(corsOptions));
4940
- if (rateLimit2 && process.env.NODE_ENV !== "development") {
4941
- const generalLimiter = createRateLimiter({
4942
- windowMs: rateLimit2.windowMs,
4943
- max: rateLimit2.max
4944
- });
4945
- app.use(generalLimiter);
6109
+ if (rateLimit2) {
6110
+ const shouldApply = process.env.NODE_ENV !== "development" || process.env.ENABLE_RATE_LIMIT === "true";
6111
+ if (shouldApply) {
6112
+ try {
6113
+ const generalLimiter = createRateLimiterFromConfig(rateLimit2, false);
6114
+ if (generalLimiter) {
6115
+ app.use(generalLimiter);
6116
+ const logger5 = createModuleLogger("server");
6117
+ logger5.info("Rate limiting enabled", {
6118
+ windowMs: rateLimit2.windowMs ?? 15 * 60 * 1e3,
6119
+ max: rateLimit2.max ?? 100,
6120
+ apiMax: rateLimit2.apiMax,
6121
+ strictMax: rateLimit2.strictMax,
6122
+ strictPatterns: rateLimit2.strictPatterns?.length ?? 0
6123
+ });
6124
+ }
6125
+ } catch (error) {
6126
+ const logger5 = createModuleLogger("server");
6127
+ logger5.error("Failed to setup rate limiting", {
6128
+ error: error instanceof Error ? error.message : String(error)
6129
+ });
6130
+ }
6131
+ }
4946
6132
  }
4947
6133
  app.use((0, import_cookie_parser.default)());
4948
6134
  app.use(import_express2.default.json({ limit: bodyLimit }));
@@ -4957,22 +6143,31 @@ var setupApplication = async ({
4957
6143
 
4958
6144
  // src/server.ts
4959
6145
  var import_dotenv2 = __toESM(require("dotenv"));
4960
- var envPath = import_path22.default.join(process.cwd(), ".env");
6146
+ var envPath = import_path25.default.join(process.cwd(), ".env");
4961
6147
  if (import_fs18.default.existsSync(envPath)) {
4962
6148
  import_dotenv2.default.config({ path: envPath });
4963
6149
  } else {
4964
6150
  import_dotenv2.default.config();
4965
6151
  }
4966
- var logger3 = createModuleLogger("server");
6152
+ var logger4 = createModuleLogger("server");
4967
6153
  async function startServer(options = {}) {
4968
6154
  const isDev = options.isDev ?? process.env.NODE_ENV === "development";
4969
6155
  const projectRoot = options.rootDir ?? process.cwd();
4970
- const config = options.config ?? loadConfig(projectRoot);
6156
+ let config;
6157
+ try {
6158
+ config = options.config ?? loadConfig(projectRoot);
6159
+ } catch (error) {
6160
+ if (error instanceof ConfigValidationError) {
6161
+ console.error("\n" + error.message + "\n");
6162
+ process.exit(1);
6163
+ }
6164
+ throw error;
6165
+ }
4971
6166
  const port = options.port ?? (process.env.PORT ? parseInt(process.env.PORT, 10) : void 0) ?? config.server.port;
4972
6167
  const host = process.env.HOST ?? (!isDev ? "0.0.0.0" : void 0) ?? config.server.host;
4973
- const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : import_path22.default.join(getBuildDir(projectRoot, config), "server"));
6168
+ const appDir = options.appDir ?? (isDev ? getAppDir(projectRoot, config) : import_path25.default.join(getBuildDir(projectRoot, config), "server"));
4974
6169
  if (!isDev && !import_fs18.default.existsSync(appDir)) {
4975
- logger3.error("Compiled directory not found", void 0, {
6170
+ logger4.error("Compiled directory not found", void 0, {
4976
6171
  buildDir: config.directories.build,
4977
6172
  appDir,
4978
6173
  environment: "production"
@@ -4989,7 +6184,7 @@ async function startServer(options = {}) {
4989
6184
  isDev,
4990
6185
  config
4991
6186
  });
4992
- const routeLoader = isDev ? new FilesystemRouteLoader(appDir) : new ManifestRouteLoader(projectRoot);
6187
+ const routeLoader = isDev ? new FilesystemRouteLoader(appDir, projectRoot) : new ManifestRouteLoader(projectRoot);
4993
6188
  setupWssEvents({
4994
6189
  httpServer,
4995
6190
  wssRoutes
@@ -5041,10 +6236,10 @@ async function startProdServer(options = {}) {
5041
6236
  }
5042
6237
 
5043
6238
  // modules/build/ssg/builder.ts
5044
- var import_path26 = __toESM(require("path"));
6239
+ var import_path29 = __toESM(require("path"));
5045
6240
 
5046
6241
  // modules/build/ssg/path.ts
5047
- var import_path23 = __toESM(require("path"));
6242
+ var import_path26 = __toESM(require("path"));
5048
6243
  function buildPathFromPattern(pattern, params) {
5049
6244
  const segments = pattern.split("/").filter(Boolean);
5050
6245
  const parts = [];
@@ -5073,12 +6268,12 @@ function buildPathFromPattern(pattern, params) {
5073
6268
  }
5074
6269
  function pathToOutDir(baseDir, urlPath) {
5075
6270
  const clean = urlPath === "/" ? "" : urlPath.replace(/^\/+/, "");
5076
- return import_path23.default.join(baseDir, clean);
6271
+ return import_path26.default.join(baseDir, clean);
5077
6272
  }
5078
6273
 
5079
6274
  // modules/build/ssg/renderer.ts
5080
6275
  var import_fs19 = __toESM(require("fs"));
5081
- var import_path24 = __toESM(require("path"));
6276
+ var import_path27 = __toESM(require("path"));
5082
6277
  var import_server3 = require("react-dom/server");
5083
6278
  init_globals();
5084
6279
  async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params) {
@@ -5095,6 +6290,10 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
5095
6290
  chunkHref = `${STATIC_PATH}/${chunkName}.js`;
5096
6291
  }
5097
6292
  }
6293
+ const entrypointFiles = [];
6294
+ if (assetManifest?.entrypoints?.client) {
6295
+ entrypointFiles.push(...assetManifest.entrypoints.client.map((file) => `${STATIC_PATH}/${file}`));
6296
+ }
5098
6297
  const req = {
5099
6298
  method: "GET",
5100
6299
  headers: {},
@@ -5124,32 +6323,79 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
5124
6323
  })
5125
6324
  );
5126
6325
  }
6326
+ const layoutProps = {};
6327
+ const layoutMetadata = [];
6328
+ if (route.layoutServerHooks && route.layoutServerHooks.length > 0) {
6329
+ for (let i = 0; i < route.layoutServerHooks.length; i++) {
6330
+ const layoutServerHook = route.layoutServerHooks[i];
6331
+ if (layoutServerHook) {
6332
+ try {
6333
+ const layoutResult = await layoutServerHook(ctx);
6334
+ if (layoutResult.props) {
6335
+ Object.assign(layoutProps, layoutResult.props);
6336
+ }
6337
+ if (layoutResult.metadata) {
6338
+ layoutMetadata.push(layoutResult.metadata);
6339
+ }
6340
+ } catch (error) {
6341
+ console.warn(
6342
+ `\u26A0\uFE0F [framework][ssg] Layout server hook ${i} failed for route ${route.pattern}:`,
6343
+ error instanceof Error ? error.message : String(error)
6344
+ );
6345
+ if (error instanceof Error && error.stack) {
6346
+ console.warn(` Stack: ${error.stack.split("\n").slice(0, 3).join("\n ")}`);
6347
+ }
6348
+ }
6349
+ }
6350
+ }
6351
+ }
5127
6352
  let loaderResult = { props: {} };
5128
6353
  if (route.loader) {
5129
6354
  loaderResult = await route.loader(ctx);
5130
6355
  }
5131
- if (loaderResult.redirect || loaderResult.notFound) {
6356
+ const combinedProps = {
6357
+ ...layoutProps,
6358
+ ...loaderResult.props || {}
6359
+ };
6360
+ let combinedMetadata = null;
6361
+ for (const layoutMeta of layoutMetadata) {
6362
+ if (layoutMeta) {
6363
+ combinedMetadata = mergeMetadata(combinedMetadata, layoutMeta);
6364
+ }
6365
+ }
6366
+ if (loaderResult.metadata) {
6367
+ combinedMetadata = mergeMetadata(combinedMetadata, loaderResult.metadata);
6368
+ }
6369
+ const combinedLoaderResult = {
6370
+ ...loaderResult,
6371
+ props: combinedProps,
6372
+ metadata: combinedMetadata
6373
+ };
6374
+ if (combinedLoaderResult.redirect || combinedLoaderResult.notFound) {
5132
6375
  return;
5133
6376
  }
5134
- const initialData = buildInitialData(urlPath, params, loaderResult);
6377
+ const initialData = buildInitialData(urlPath, params, combinedLoaderResult);
5135
6378
  const routerData = buildRouterData(req);
5136
6379
  const appTree = buildAppTree(route, params, initialData.props);
5137
6380
  const documentTree = createDocumentTree({
5138
6381
  appTree,
5139
6382
  initialData,
5140
6383
  routerData,
5141
- meta: loaderResult.metadata,
6384
+ meta: combinedLoaderResult.metadata,
5142
6385
  titleFallback: "My Framework Dev",
5143
6386
  descriptionFallback: "Static page generated by @lolyjs/core.",
5144
6387
  chunkHref,
6388
+ entrypointFiles,
5145
6389
  clientJsPath,
5146
- clientCssPath
6390
+ clientCssPath,
6391
+ includeInlineScripts: true
6392
+ // SSG needs inline scripts (renderToString doesn't support bootstrapScripts)
5147
6393
  });
5148
6394
  const html = "<!DOCTYPE html>" + (0, import_server3.renderToString)(documentTree);
5149
6395
  const dir = pathToOutDir(ssgOutDir, urlPath);
5150
6396
  ensureDir(dir);
5151
- const htmlFile = import_path24.default.join(dir, "index.html");
5152
- const dataFile = import_path24.default.join(dir, "data.json");
6397
+ const htmlFile = import_path27.default.join(dir, "index.html");
6398
+ const dataFile = import_path27.default.join(dir, "data.json");
5153
6399
  import_fs19.default.writeFileSync(htmlFile, html, "utf-8");
5154
6400
  import_fs19.default.writeFileSync(dataFile, JSON.stringify(initialData, null, 2), "utf-8");
5155
6401
  }
@@ -5157,7 +6403,7 @@ async function renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params)
5157
6403
  // modules/build/ssg/builder.ts
5158
6404
  init_globals();
5159
6405
  async function buildStaticPages(projectRoot, routes) {
5160
- const ssgOutDir = import_path26.default.join(projectRoot, BUILD_FOLDER_NAME, "ssg");
6406
+ const ssgOutDir = import_path29.default.join(projectRoot, BUILD_FOLDER_NAME, "ssg");
5161
6407
  ensureDir(ssgOutDir);
5162
6408
  for (const route of routes) {
5163
6409
  if (route.dynamic !== "force-static") continue;
@@ -5167,12 +6413,15 @@ async function buildStaticPages(projectRoot, routes) {
5167
6413
  } else {
5168
6414
  if (!route.generateStaticParams) {
5169
6415
  console.warn(
5170
- `[framework][ssg] Route ${route.pattern} is marked as force-static but has no generateStaticParams function. Skipping.`
6416
+ `\u26A0\uFE0F [framework][ssg] Route "${route.pattern}" is marked as force-static but has no generateStaticParams function`
5171
6417
  );
6418
+ console.warn(` \u{1F4A1} Add a generateStaticParams export to enable static generation for this route`);
6419
+ console.warn(` Skipping this route...
6420
+ `);
5172
6421
  continue;
5173
6422
  }
5174
6423
  try {
5175
- console.log(`[framework][ssg] Generating static params for route: ${route.pattern}`);
6424
+ console.log(`\u{1F4E6} [framework][ssg] Generating static params for route: ${route.pattern}`);
5176
6425
  let timeoutId = null;
5177
6426
  const timeoutPromise = new Promise((_, reject) => {
5178
6427
  timeoutId = setTimeout(() => {
@@ -5187,12 +6436,16 @@ async function buildStaticPages(projectRoot, routes) {
5187
6436
  clearTimeout(timeoutId);
5188
6437
  }
5189
6438
  allParams = sp;
5190
- console.log(`[framework][ssg] Generated ${sp.length} static params for route: ${route.pattern}`);
6439
+ console.log(` \u2705 Generated ${sp.length} static params for route: ${route.pattern}`);
5191
6440
  } catch (error) {
5192
- console.error(
5193
- `[framework][ssg] Error generating static params for route ${route.pattern}:`,
5194
- error
5195
- );
6441
+ console.error(`
6442
+ \u274C [framework][ssg] Error generating static params for route "${route.pattern}":`);
6443
+ console.error(error instanceof Error ? error.message : String(error));
6444
+ if (error instanceof Error && error.stack) {
6445
+ console.error(` Stack: ${error.stack.split("\n").slice(0, 3).join("\n ")}`);
6446
+ }
6447
+ console.error(`\u{1F4A1} Check your generateStaticParams function for this route
6448
+ `);
5196
6449
  throw error;
5197
6450
  }
5198
6451
  }
@@ -5201,11 +6454,11 @@ async function buildStaticPages(projectRoot, routes) {
5201
6454
  await renderStaticRoute(projectRoot, ssgOutDir, route, urlPath, params);
5202
6455
  }
5203
6456
  }
5204
- console.log(`[framework][ssg] Finished building all static pages`);
6457
+ console.log(`\u2705 [framework][ssg] Finished building all static pages`);
5205
6458
  }
5206
6459
 
5207
6460
  // modules/build/bundler/server.ts
5208
- var import_path28 = __toESM(require("path"));
6461
+ var import_path31 = __toESM(require("path"));
5209
6462
  var import_fs20 = __toESM(require("fs"));
5210
6463
  var import_esbuild = __toESM(require("esbuild"));
5211
6464
  init_globals();
@@ -5215,7 +6468,7 @@ function collectAppSources(appDir) {
5215
6468
  function walk(dir) {
5216
6469
  const items = import_fs20.default.readdirSync(dir, { withFileTypes: true });
5217
6470
  for (const item of items) {
5218
- const full = import_path28.default.join(dir, item.name);
6471
+ const full = import_path31.default.join(dir, item.name);
5219
6472
  if (item.isDirectory()) {
5220
6473
  walk(full);
5221
6474
  continue;
@@ -5232,7 +6485,7 @@ function collectAppSources(appDir) {
5232
6485
  return entries;
5233
6486
  }
5234
6487
  async function buildServerApp(projectRoot, appDir) {
5235
- const outDir = import_path28.default.join(projectRoot, BUILD_FOLDER_NAME, "server");
6488
+ const outDir = import_path31.default.join(projectRoot, BUILD_FOLDER_NAME, "server");
5236
6489
  const entryPoints = collectAppSources(appDir);
5237
6490
  ensureDir(outDir);
5238
6491
  if (entryPoints.length === 0) {
@@ -5250,12 +6503,12 @@ async function buildServerApp(projectRoot, appDir) {
5250
6503
  bundle: true,
5251
6504
  splitting: false,
5252
6505
  logLevel: "info",
5253
- tsconfig: import_path28.default.join(projectRoot, "tsconfig.json"),
6506
+ tsconfig: import_path31.default.join(projectRoot, "tsconfig.json"),
5254
6507
  packages: "external"
5255
6508
  });
5256
6509
  for (const fileName of SERVER_FILES) {
5257
- const initTS = import_path28.default.join(projectRoot, `${fileName}.ts`);
5258
- const initJS = import_path28.default.join(outDir, `${fileName}.js`);
6510
+ const initTS = import_path31.default.join(projectRoot, `${fileName}.ts`);
6511
+ const initJS = import_path31.default.join(outDir, `${fileName}.js`);
5259
6512
  if (import_fs20.default.existsSync(initTS)) {
5260
6513
  await import_esbuild.default.build({
5261
6514
  entryPoints: [initTS],
@@ -5267,7 +6520,7 @@ async function buildServerApp(projectRoot, appDir) {
5267
6520
  sourcemap: true,
5268
6521
  bundle: false,
5269
6522
  logLevel: "info",
5270
- tsconfig: import_path28.default.join(projectRoot, "tsconfig.json")
6523
+ tsconfig: import_path31.default.join(projectRoot, "tsconfig.json")
5271
6524
  });
5272
6525
  }
5273
6526
  }
@@ -5410,22 +6663,158 @@ function matchRouteClient(pathWithSearch, routes) {
5410
6663
  }
5411
6664
 
5412
6665
  // modules/runtime/client/metadata.ts
6666
+ function getOrCreateMeta(selector, attributes) {
6667
+ let meta = document.querySelector(selector);
6668
+ if (!meta) {
6669
+ meta = document.createElement("meta");
6670
+ if (attributes.name) meta.name = attributes.name;
6671
+ if (attributes.property) meta.setAttribute("property", attributes.property);
6672
+ if (attributes.httpEquiv) meta.httpEquiv = attributes.httpEquiv;
6673
+ document.head.appendChild(meta);
6674
+ }
6675
+ return meta;
6676
+ }
6677
+ function getOrCreateLink(rel, href) {
6678
+ const selector = `link[rel="${rel}"]`;
6679
+ let link = document.querySelector(selector);
6680
+ if (!link) {
6681
+ link = document.createElement("link");
6682
+ link.rel = rel;
6683
+ link.href = href;
6684
+ document.head.appendChild(link);
6685
+ } else {
6686
+ link.href = href;
6687
+ }
6688
+ return link;
6689
+ }
5413
6690
  function applyMetadata(md) {
5414
6691
  if (!md) return;
5415
6692
  if (md.title) {
5416
6693
  document.title = md.title;
5417
6694
  }
5418
6695
  if (md.description) {
5419
- let meta = document.querySelector(
5420
- 'meta[name="description"]'
5421
- );
5422
- if (!meta) {
5423
- meta = document.createElement("meta");
5424
- meta.name = "description";
5425
- document.head.appendChild(meta);
5426
- }
6696
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
5427
6697
  meta.content = md.description;
5428
6698
  }
6699
+ if (md.robots) {
6700
+ const meta = getOrCreateMeta('meta[name="robots"]', { name: "robots" });
6701
+ meta.content = md.robots;
6702
+ }
6703
+ if (md.themeColor) {
6704
+ const meta = getOrCreateMeta('meta[name="theme-color"]', { name: "theme-color" });
6705
+ meta.content = md.themeColor;
6706
+ }
6707
+ if (md.viewport) {
6708
+ const meta = getOrCreateMeta('meta[name="viewport"]', { name: "viewport" });
6709
+ meta.content = md.viewport;
6710
+ }
6711
+ if (md.canonical) {
6712
+ getOrCreateLink("canonical", md.canonical);
6713
+ }
6714
+ if (md.openGraph) {
6715
+ const og = md.openGraph;
6716
+ if (og.title) {
6717
+ const meta = getOrCreateMeta('meta[property="og:title"]', { property: "og:title" });
6718
+ meta.content = og.title;
6719
+ }
6720
+ if (og.description) {
6721
+ const meta = getOrCreateMeta('meta[property="og:description"]', { property: "og:description" });
6722
+ meta.content = og.description;
6723
+ }
6724
+ if (og.type) {
6725
+ const meta = getOrCreateMeta('meta[property="og:type"]', { property: "og:type" });
6726
+ meta.content = og.type;
6727
+ }
6728
+ if (og.url) {
6729
+ const meta = getOrCreateMeta('meta[property="og:url"]', { property: "og:url" });
6730
+ meta.content = og.url;
6731
+ }
6732
+ if (og.image) {
6733
+ if (typeof og.image === "string") {
6734
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
6735
+ meta.content = og.image;
6736
+ } else {
6737
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
6738
+ meta.content = og.image.url;
6739
+ if (og.image.width) {
6740
+ const metaWidth = getOrCreateMeta('meta[property="og:image:width"]', { property: "og:image:width" });
6741
+ metaWidth.content = String(og.image.width);
6742
+ }
6743
+ if (og.image.height) {
6744
+ const metaHeight = getOrCreateMeta('meta[property="og:image:height"]', { property: "og:image:height" });
6745
+ metaHeight.content = String(og.image.height);
6746
+ }
6747
+ if (og.image.alt) {
6748
+ const metaAlt = getOrCreateMeta('meta[property="og:image:alt"]', { property: "og:image:alt" });
6749
+ metaAlt.content = og.image.alt;
6750
+ }
6751
+ }
6752
+ }
6753
+ if (og.siteName) {
6754
+ const meta = getOrCreateMeta('meta[property="og:site_name"]', { property: "og:site_name" });
6755
+ meta.content = og.siteName;
6756
+ }
6757
+ if (og.locale) {
6758
+ const meta = getOrCreateMeta('meta[property="og:locale"]', { property: "og:locale" });
6759
+ meta.content = og.locale;
6760
+ }
6761
+ }
6762
+ if (md.twitter) {
6763
+ const twitter = md.twitter;
6764
+ if (twitter.card) {
6765
+ const meta = getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" });
6766
+ meta.content = twitter.card;
6767
+ }
6768
+ if (twitter.title) {
6769
+ const meta = getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" });
6770
+ meta.content = twitter.title;
6771
+ }
6772
+ if (twitter.description) {
6773
+ const meta = getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" });
6774
+ meta.content = twitter.description;
6775
+ }
6776
+ if (twitter.image) {
6777
+ const meta = getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" });
6778
+ meta.content = twitter.image;
6779
+ }
6780
+ if (twitter.imageAlt) {
6781
+ const meta = getOrCreateMeta('meta[name="twitter:image:alt"]', { name: "twitter:image:alt" });
6782
+ meta.content = twitter.imageAlt;
6783
+ }
6784
+ if (twitter.site) {
6785
+ const meta = getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" });
6786
+ meta.content = twitter.site;
6787
+ }
6788
+ if (twitter.creator) {
6789
+ const meta = getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" });
6790
+ meta.content = twitter.creator;
6791
+ }
6792
+ }
6793
+ if (md.metaTags && Array.isArray(md.metaTags)) {
6794
+ md.metaTags.forEach((tag) => {
6795
+ let selector = "";
6796
+ if (tag.name) {
6797
+ selector = `meta[name="${tag.name}"]`;
6798
+ } else if (tag.property) {
6799
+ selector = `meta[property="${tag.property}"]`;
6800
+ } else if (tag.httpEquiv) {
6801
+ selector = `meta[http-equiv="${tag.httpEquiv}"]`;
6802
+ }
6803
+ if (selector) {
6804
+ const meta = getOrCreateMeta(selector, {
6805
+ name: tag.name,
6806
+ property: tag.property,
6807
+ httpEquiv: tag.httpEquiv
6808
+ });
6809
+ meta.content = tag.content;
6810
+ }
6811
+ });
6812
+ }
6813
+ if (md.links && Array.isArray(md.links)) {
6814
+ md.links.forEach((link) => {
6815
+ getOrCreateLink(link.rel, link.href);
6816
+ });
6817
+ }
5429
6818
  }
5430
6819
 
5431
6820
  // modules/runtime/client/AppShell.tsx
@@ -5642,10 +7031,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
5642
7031
  });
5643
7032
  return true;
5644
7033
  } catch (loadError) {
5645
- console.error(
5646
- "[client] Error loading error route components:",
5647
- loadError
5648
- );
7034
+ console.error("\n\u274C [client] Error loading error route components:");
7035
+ console.error(loadError);
7036
+ if (loadError instanceof Error) {
7037
+ console.error(` Message: ${loadError.message}`);
7038
+ if (loadError.stack) {
7039
+ console.error(` Stack: ${loadError.stack.split("\n").slice(0, 3).join("\n ")}`);
7040
+ }
7041
+ }
7042
+ console.error("\u{1F4A1} Falling back to full page reload\n");
5649
7043
  window.location.href = nextUrl;
5650
7044
  return false;
5651
7045
  }
@@ -5750,7 +7144,11 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
5750
7144
  searchParams: Object.fromEntries(url.searchParams.entries())
5751
7145
  };
5752
7146
  setRouterData(routerData);
5753
- const components = await matched.route.load();
7147
+ const prefetched = prefetchedRoutes.get(matched.route);
7148
+ const components = prefetched ? await prefetched : await matched.route.load();
7149
+ if (!prefetched) {
7150
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
7151
+ }
5754
7152
  window.scrollTo({
5755
7153
  top: 0,
5756
7154
  behavior: "smooth"
@@ -5789,7 +7187,7 @@ async function navigate(nextUrl, handlers, options) {
5789
7187
  }
5790
7188
  }
5791
7189
  if (!ok) {
5792
- if (json && json.redirect) {
7190
+ if (json?.redirect) {
5793
7191
  window.location.href = json.redirect.destination;
5794
7192
  return;
5795
7193
  }
@@ -5810,6 +7208,47 @@ async function navigate(nextUrl, handlers, options) {
5810
7208
  window.location.href = nextUrl;
5811
7209
  }
5812
7210
  }
7211
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
7212
+ function prefetchRoute(url, routes, notFoundRoute) {
7213
+ const [pathname] = url.split("?");
7214
+ const matched = matchRouteClient(pathname, routes);
7215
+ if (!matched) {
7216
+ if (notFoundRoute) {
7217
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
7218
+ if (!existing2) {
7219
+ const promise = notFoundRoute.load();
7220
+ prefetchedRoutes.set(notFoundRoute, promise);
7221
+ }
7222
+ }
7223
+ return;
7224
+ }
7225
+ const existing = prefetchedRoutes.get(matched.route);
7226
+ if (!existing) {
7227
+ const promise = matched.route.load();
7228
+ prefetchedRoutes.set(matched.route, promise);
7229
+ }
7230
+ }
7231
+ function createHoverHandler(routes, notFoundRoute) {
7232
+ return function handleHover(ev) {
7233
+ try {
7234
+ const target = ev.target;
7235
+ if (!target) return;
7236
+ const anchor = target.closest("a[href]");
7237
+ if (!anchor) return;
7238
+ const href = anchor.getAttribute("href");
7239
+ if (!href) return;
7240
+ if (href.startsWith("#")) return;
7241
+ const url = new URL(href, window.location.href);
7242
+ if (url.origin !== window.location.origin) return;
7243
+ if (anchor.target && anchor.target !== "_self") return;
7244
+ const nextUrl = url.pathname + url.search;
7245
+ const currentUrl = window.location.pathname + window.location.search;
7246
+ if (nextUrl === currentUrl) return;
7247
+ prefetchRoute(nextUrl, routes, notFoundRoute);
7248
+ } catch (error) {
7249
+ }
7250
+ };
7251
+ }
5813
7252
  function createClickHandler(navigate2) {
5814
7253
  return function handleClick(ev) {
5815
7254
  try {
@@ -5920,17 +7359,20 @@ function AppShell({
5920
7359
  }
5921
7360
  const handleClick = createClickHandler(handleNavigateInternal);
5922
7361
  const handlePopState = createPopStateHandler(handleNavigateInternal);
7362
+ const handleHover = createHoverHandler(routes, notFoundRoute);
5923
7363
  window.addEventListener("click", handleClick, false);
5924
7364
  window.addEventListener("popstate", handlePopState, false);
7365
+ window.addEventListener("mouseover", handleHover, false);
5925
7366
  return () => {
5926
7367
  isMounted = false;
5927
7368
  window.removeEventListener("click", handleClick, false);
5928
7369
  window.removeEventListener("popstate", handlePopState, false);
7370
+ window.removeEventListener("mouseover", handleHover, false);
5929
7371
  };
5930
- }, []);
7372
+ }, [routes, notFoundRoute]);
5931
7373
  (0, import_react3.useEffect)(() => {
5932
7374
  const handleDataRefresh = () => {
5933
- const freshData = window?.__FW_DATA__;
7375
+ const freshData = window[WINDOW_DATA_KEY2];
5934
7376
  if (!freshData) return;
5935
7377
  const currentPathname = window.location.pathname;
5936
7378
  const freshPathname = freshData.pathname;
@@ -5957,6 +7399,91 @@ function AppShell({
5957
7399
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterView, { state }, routeKey) });
5958
7400
  }
5959
7401
 
7402
+ // modules/runtime/client/hot-reload.ts
7403
+ function setupHotReload2() {
7404
+ const nodeEnv = process.env.NODE_ENV || "production";
7405
+ const isDev = nodeEnv === "development";
7406
+ console.log(`[hot-reload] NODE_ENV: ${nodeEnv}, isDev: ${isDev}`);
7407
+ if (!isDev) {
7408
+ console.log("[hot-reload] Skipping hot reload setup (not in development mode)");
7409
+ return;
7410
+ }
7411
+ console.log("[hot-reload] Setting up hot reload client...");
7412
+ let eventSource = null;
7413
+ let reloadTimeout = null;
7414
+ let reconnectTimeout = null;
7415
+ let reconnectAttempts = 0;
7416
+ const MAX_RECONNECT_ATTEMPTS = 10;
7417
+ const RECONNECT_DELAY = 1e3;
7418
+ const RELOAD_DELAY = 100;
7419
+ function connect() {
7420
+ try {
7421
+ if (eventSource) {
7422
+ console.log("[hot-reload] Closing existing EventSource connection");
7423
+ eventSource.close();
7424
+ }
7425
+ const endpoint = "/__fw/hot";
7426
+ eventSource = new EventSource(endpoint);
7427
+ eventSource.addEventListener("ping", (event) => {
7428
+ if ("data" in event) {
7429
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
7430
+ }
7431
+ reconnectAttempts = 0;
7432
+ });
7433
+ eventSource.addEventListener("message", (event) => {
7434
+ const data = event.data;
7435
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
7436
+ const filePath = data.slice(7);
7437
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
7438
+ if (reloadTimeout) {
7439
+ clearTimeout(reloadTimeout);
7440
+ }
7441
+ reloadTimeout = setTimeout(() => {
7442
+ try {
7443
+ window.location.reload();
7444
+ } catch (error) {
7445
+ console.error("[hot-reload] \u274C Error reloading page:", error);
7446
+ setTimeout(() => window.location.reload(), 100);
7447
+ }
7448
+ }, RELOAD_DELAY);
7449
+ }
7450
+ });
7451
+ eventSource.onopen = () => {
7452
+ reconnectAttempts = 0;
7453
+ };
7454
+ eventSource.onerror = (error) => {
7455
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
7456
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
7457
+ if (eventSource?.readyState === EventSource.CONNECTING) {
7458
+ console.log("[hot-reload] \u23F3 Still connecting...");
7459
+ return;
7460
+ } else if (eventSource?.readyState === EventSource.OPEN) {
7461
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
7462
+ } else {
7463
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
7464
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
7465
+ reconnectAttempts++;
7466
+ const delay = RECONNECT_DELAY * reconnectAttempts;
7467
+ if (reconnectTimeout) {
7468
+ clearTimeout(reconnectTimeout);
7469
+ }
7470
+ reconnectTimeout = setTimeout(() => {
7471
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
7472
+ connect();
7473
+ }, delay);
7474
+ } else {
7475
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
7476
+ }
7477
+ }
7478
+ };
7479
+ } catch (error) {
7480
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
7481
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
7482
+ }
7483
+ }
7484
+ connect();
7485
+ }
7486
+
5960
7487
  // modules/runtime/client/bootstrap.tsx
5961
7488
  var import_jsx_runtime3 = require("react/jsx-runtime");
5962
7489
  async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute, errorRoute) {
@@ -5998,101 +7525,91 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
5998
7525
  props: initialData?.props ?? {}
5999
7526
  };
6000
7527
  }
6001
- function setupHotReload2() {
6002
- const nodeEnv = typeof process !== "undefined" && process.env?.NODE_ENV || "production";
6003
- const isDev = nodeEnv !== "production";
6004
- if (!isDev) {
6005
- return;
7528
+ function initializeRouterData(initialUrl, initialData) {
7529
+ let routerData = getRouterData();
7530
+ if (!routerData) {
7531
+ const url = new URL(initialUrl, window.location.origin);
7532
+ routerData = {
7533
+ pathname: url.pathname,
7534
+ params: initialData?.params || {},
7535
+ searchParams: Object.fromEntries(url.searchParams.entries())
7536
+ };
7537
+ setRouterData(routerData);
6006
7538
  }
7539
+ }
7540
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
6007
7541
  try {
6008
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
6009
- const eventSource = new EventSource("/__fw/hot");
6010
- let reloadTimeout = null;
6011
- eventSource.addEventListener("message", (event) => {
6012
- const data = event.data;
6013
- if (data && data.startsWith("reload:")) {
6014
- const filePath = data.slice(7);
6015
- console.log(`[hot-reload] File changed: ${filePath}`);
6016
- if (reloadTimeout) {
6017
- clearTimeout(reloadTimeout);
6018
- }
6019
- reloadTimeout = setTimeout(() => {
6020
- console.log("[hot-reload] Reloading page...");
6021
- window.location.reload();
6022
- }, 500);
6023
- }
6024
- });
6025
- eventSource.addEventListener("ping", () => {
6026
- console.log("[hot-reload] \u2713 Connected to hot reload server");
6027
- });
6028
- eventSource.onopen = () => {
6029
- console.log("[hot-reload] \u2713 SSE connection opened");
6030
- };
6031
- eventSource.onerror = (error) => {
6032
- const states = ["CONNECTING", "OPEN", "CLOSED"];
6033
- const state = states[eventSource.readyState] || "UNKNOWN";
6034
- if (eventSource.readyState === EventSource.CONNECTING) {
6035
- console.log("[hot-reload] Connecting...");
6036
- } else if (eventSource.readyState === EventSource.OPEN) {
6037
- console.warn("[hot-reload] Connection error (but connection is open):", error);
6038
- } else {
6039
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
7542
+ const initialState = await loadInitialRoute(
7543
+ initialUrl,
7544
+ initialData,
7545
+ routes,
7546
+ notFoundRoute,
7547
+ errorRoute
7548
+ );
7549
+ if (initialData?.metadata) {
7550
+ try {
7551
+ applyMetadata(initialData.metadata);
7552
+ } catch (metadataError) {
7553
+ console.warn("[client] Error applying metadata:", metadataError);
6040
7554
  }
6041
- };
7555
+ }
7556
+ (0, import_client5.hydrateRoot)(
7557
+ container,
7558
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
7559
+ AppShell,
7560
+ {
7561
+ initialState,
7562
+ routes,
7563
+ notFoundRoute,
7564
+ errorRoute
7565
+ }
7566
+ )
7567
+ );
6042
7568
  } catch (error) {
6043
- console.log("[hot-reload] EventSource not supported or error:", error);
7569
+ console.error(
7570
+ "[client] Error loading initial route components for",
7571
+ initialUrl,
7572
+ error
7573
+ );
7574
+ throw error;
6044
7575
  }
6045
7576
  }
6046
7577
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
6047
- console.log("[client] Bootstrap starting, setting up hot reload...");
6048
7578
  setupHotReload2();
6049
- (async function bootstrap() {
6050
- const container = document.getElementById(APP_CONTAINER_ID2);
6051
- const initialData = getWindowData();
6052
- if (!container) {
6053
- console.error(`Container #${APP_CONTAINER_ID2} not found for hydration`);
6054
- return;
6055
- }
6056
- const initialUrl = window.location.pathname + window.location.search;
6057
- let routerData = getRouterData();
6058
- if (!routerData) {
6059
- const url = new URL(initialUrl, window.location.origin);
6060
- routerData = {
6061
- pathname: url.pathname,
6062
- params: initialData?.params || {},
6063
- searchParams: Object.fromEntries(url.searchParams.entries())
6064
- };
6065
- setRouterData(routerData);
6066
- }
7579
+ (async () => {
6067
7580
  try {
6068
- const initialState = await loadInitialRoute(
7581
+ const container = document.getElementById(APP_CONTAINER_ID2);
7582
+ if (!container) {
7583
+ console.error(`
7584
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID2} not found`);
7585
+ console.error("\u{1F4A1} This usually means:");
7586
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
7587
+ console.error(" \u2022 The container was removed before hydration");
7588
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
7589
+ return;
7590
+ }
7591
+ const initialData = getWindowData();
7592
+ const initialUrl = window.location.pathname + window.location.search;
7593
+ initializeRouterData(initialUrl, initialData);
7594
+ await hydrateInitialRoute(
7595
+ container,
6069
7596
  initialUrl,
6070
7597
  initialData,
6071
7598
  routes,
6072
7599
  notFoundRoute,
6073
7600
  errorRoute
6074
7601
  );
6075
- if (initialData?.metadata) {
6076
- applyMetadata(initialData.metadata);
6077
- }
6078
- (0, import_client5.hydrateRoot)(
6079
- container,
6080
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
6081
- AppShell,
6082
- {
6083
- initialState,
6084
- routes,
6085
- notFoundRoute,
6086
- errorRoute
6087
- }
6088
- )
6089
- );
6090
7602
  } catch (error) {
6091
- console.error(
6092
- "[client] Error loading initial route components for",
6093
- initialUrl,
6094
- error
6095
- );
7603
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
7604
+ console.error(error);
7605
+ if (error instanceof Error) {
7606
+ console.error("\nError details:");
7607
+ console.error(` Message: ${error.message}`);
7608
+ if (error.stack) {
7609
+ console.error(` Stack: ${error.stack}`);
7610
+ }
7611
+ }
7612
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
6096
7613
  window.location.reload();
6097
7614
  }
6098
7615
  })();
@@ -6120,11 +7637,11 @@ var ValidationError = class extends Error {
6120
7637
  format() {
6121
7638
  const formatted = {};
6122
7639
  for (const error of this.errors) {
6123
- const path25 = error.path.join(".");
6124
- if (!formatted[path25]) {
6125
- formatted[path25] = [];
7640
+ const path28 = error.path.join(".");
7641
+ if (!formatted[path28]) {
7642
+ formatted[path28] = [];
6126
7643
  }
6127
- formatted[path25].push(error.message);
7644
+ formatted[path28].push(error.message);
6128
7645
  }
6129
7646
  return formatted;
6130
7647
  }