@lolyjs/core 0.2.0-alpha.3 → 0.2.0-alpha.30

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.
Files changed (46) hide show
  1. package/README.md +1463 -761
  2. package/dist/{bootstrap-BiCQmSkx.d.mts → bootstrap-BfGTMUkj.d.mts} +19 -0
  3. package/dist/{bootstrap-BiCQmSkx.d.ts → bootstrap-BfGTMUkj.d.ts} +19 -0
  4. package/dist/cli.cjs +15701 -2448
  5. package/dist/cli.cjs.map +1 -1
  6. package/dist/cli.js +15704 -2441
  7. package/dist/cli.js.map +1 -1
  8. package/dist/index.cjs +17861 -4115
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.mts +323 -55
  11. package/dist/index.d.ts +323 -55
  12. package/dist/index.js +17982 -4227
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.types-B9j4OQft.d.mts +222 -0
  15. package/dist/index.types-B9j4OQft.d.ts +222 -0
  16. package/dist/react/cache.cjs +107 -32
  17. package/dist/react/cache.cjs.map +1 -1
  18. package/dist/react/cache.d.mts +29 -21
  19. package/dist/react/cache.d.ts +29 -21
  20. package/dist/react/cache.js +107 -32
  21. package/dist/react/cache.js.map +1 -1
  22. package/dist/react/components.cjs +11 -12
  23. package/dist/react/components.cjs.map +1 -1
  24. package/dist/react/components.js +11 -12
  25. package/dist/react/components.js.map +1 -1
  26. package/dist/react/hooks.cjs +124 -74
  27. package/dist/react/hooks.cjs.map +1 -1
  28. package/dist/react/hooks.d.mts +6 -24
  29. package/dist/react/hooks.d.ts +6 -24
  30. package/dist/react/hooks.js +122 -71
  31. package/dist/react/hooks.js.map +1 -1
  32. package/dist/react/sockets.cjs +5 -6
  33. package/dist/react/sockets.cjs.map +1 -1
  34. package/dist/react/sockets.js +5 -6
  35. package/dist/react/sockets.js.map +1 -1
  36. package/dist/react/themes.cjs +61 -18
  37. package/dist/react/themes.cjs.map +1 -1
  38. package/dist/react/themes.js +63 -20
  39. package/dist/react/themes.js.map +1 -1
  40. package/dist/runtime.cjs +531 -104
  41. package/dist/runtime.cjs.map +1 -1
  42. package/dist/runtime.d.mts +2 -2
  43. package/dist/runtime.d.ts +2 -2
  44. package/dist/runtime.js +531 -104
  45. package/dist/runtime.js.map +1 -1
  46. package/package.json +56 -14
package/dist/runtime.cjs CHANGED
@@ -29,15 +29,40 @@ var import_client = require("react-dom/client");
29
29
 
30
30
  // modules/runtime/client/constants.ts
31
31
  var WINDOW_DATA_KEY = "__FW_DATA__";
32
+ var ROUTER_DATA_KEY = "__LOLY_ROUTER_DATA__";
32
33
  var APP_CONTAINER_ID = "__app";
34
+ var ROUTER_NAVIGATE_KEY = "__LOLY_ROUTER_NAVIGATE__";
33
35
 
34
36
  // modules/runtime/client/window-data.ts
37
+ var LAYOUT_PROPS_KEY = "__FW_LAYOUT_PROPS__";
35
38
  function getWindowData() {
36
39
  if (typeof window === "undefined") {
37
40
  return null;
38
41
  }
39
42
  return window[WINDOW_DATA_KEY] ?? null;
40
43
  }
44
+ function getPreservedLayoutProps() {
45
+ if (typeof window === "undefined") {
46
+ return null;
47
+ }
48
+ return window[LAYOUT_PROPS_KEY] ?? null;
49
+ }
50
+ function setPreservedLayoutProps(props) {
51
+ if (typeof window === "undefined") {
52
+ return;
53
+ }
54
+ if (props === null) {
55
+ delete window[LAYOUT_PROPS_KEY];
56
+ } else {
57
+ window[LAYOUT_PROPS_KEY] = props;
58
+ }
59
+ }
60
+ function getRouterData() {
61
+ if (typeof window === "undefined") {
62
+ return null;
63
+ }
64
+ return window[ROUTER_DATA_KEY] ?? null;
65
+ }
41
66
  function setWindowData(data) {
42
67
  window[WINDOW_DATA_KEY] = data;
43
68
  if (typeof window !== "undefined") {
@@ -48,6 +73,16 @@ function setWindowData(data) {
48
73
  );
49
74
  }
50
75
  }
76
+ function setRouterData(data) {
77
+ window[ROUTER_DATA_KEY] = data;
78
+ if (typeof window !== "undefined") {
79
+ window.dispatchEvent(
80
+ new CustomEvent("fw-router-data-refresh", {
81
+ detail: { data }
82
+ })
83
+ );
84
+ }
85
+ }
51
86
  function getCurrentTheme() {
52
87
  return getWindowData()?.theme ?? null;
53
88
  }
@@ -93,22 +128,158 @@ function matchRouteClient(pathWithSearch, routes) {
93
128
  }
94
129
 
95
130
  // modules/runtime/client/metadata.ts
131
+ function getOrCreateMeta(selector, attributes) {
132
+ let meta = document.querySelector(selector);
133
+ if (!meta) {
134
+ meta = document.createElement("meta");
135
+ if (attributes.name) meta.name = attributes.name;
136
+ if (attributes.property) meta.setAttribute("property", attributes.property);
137
+ if (attributes.httpEquiv) meta.httpEquiv = attributes.httpEquiv;
138
+ document.head.appendChild(meta);
139
+ }
140
+ return meta;
141
+ }
142
+ function getOrCreateLink(rel, href) {
143
+ const selector = `link[rel="${rel}"]`;
144
+ let link = document.querySelector(selector);
145
+ if (!link) {
146
+ link = document.createElement("link");
147
+ link.rel = rel;
148
+ link.href = href;
149
+ document.head.appendChild(link);
150
+ } else {
151
+ link.href = href;
152
+ }
153
+ return link;
154
+ }
96
155
  function applyMetadata(md) {
97
156
  if (!md) return;
98
157
  if (md.title) {
99
158
  document.title = md.title;
100
159
  }
101
160
  if (md.description) {
102
- let meta = document.querySelector(
103
- 'meta[name="description"]'
104
- );
105
- if (!meta) {
106
- meta = document.createElement("meta");
107
- meta.name = "description";
108
- document.head.appendChild(meta);
109
- }
161
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
110
162
  meta.content = md.description;
111
163
  }
164
+ if (md.robots) {
165
+ const meta = getOrCreateMeta('meta[name="robots"]', { name: "robots" });
166
+ meta.content = md.robots;
167
+ }
168
+ if (md.themeColor) {
169
+ const meta = getOrCreateMeta('meta[name="theme-color"]', { name: "theme-color" });
170
+ meta.content = md.themeColor;
171
+ }
172
+ if (md.viewport) {
173
+ const meta = getOrCreateMeta('meta[name="viewport"]', { name: "viewport" });
174
+ meta.content = md.viewport;
175
+ }
176
+ if (md.canonical) {
177
+ getOrCreateLink("canonical", md.canonical);
178
+ }
179
+ if (md.openGraph) {
180
+ const og = md.openGraph;
181
+ if (og.title) {
182
+ const meta = getOrCreateMeta('meta[property="og:title"]', { property: "og:title" });
183
+ meta.content = og.title;
184
+ }
185
+ if (og.description) {
186
+ const meta = getOrCreateMeta('meta[property="og:description"]', { property: "og:description" });
187
+ meta.content = og.description;
188
+ }
189
+ if (og.type) {
190
+ const meta = getOrCreateMeta('meta[property="og:type"]', { property: "og:type" });
191
+ meta.content = og.type;
192
+ }
193
+ if (og.url) {
194
+ const meta = getOrCreateMeta('meta[property="og:url"]', { property: "og:url" });
195
+ meta.content = og.url;
196
+ }
197
+ if (og.image) {
198
+ if (typeof og.image === "string") {
199
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
200
+ meta.content = og.image;
201
+ } else {
202
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
203
+ meta.content = og.image.url;
204
+ if (og.image.width) {
205
+ const metaWidth = getOrCreateMeta('meta[property="og:image:width"]', { property: "og:image:width" });
206
+ metaWidth.content = String(og.image.width);
207
+ }
208
+ if (og.image.height) {
209
+ const metaHeight = getOrCreateMeta('meta[property="og:image:height"]', { property: "og:image:height" });
210
+ metaHeight.content = String(og.image.height);
211
+ }
212
+ if (og.image.alt) {
213
+ const metaAlt = getOrCreateMeta('meta[property="og:image:alt"]', { property: "og:image:alt" });
214
+ metaAlt.content = og.image.alt;
215
+ }
216
+ }
217
+ }
218
+ if (og.siteName) {
219
+ const meta = getOrCreateMeta('meta[property="og:site_name"]', { property: "og:site_name" });
220
+ meta.content = og.siteName;
221
+ }
222
+ if (og.locale) {
223
+ const meta = getOrCreateMeta('meta[property="og:locale"]', { property: "og:locale" });
224
+ meta.content = og.locale;
225
+ }
226
+ }
227
+ if (md.twitter) {
228
+ const twitter = md.twitter;
229
+ if (twitter.card) {
230
+ const meta = getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" });
231
+ meta.content = twitter.card;
232
+ }
233
+ if (twitter.title) {
234
+ const meta = getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" });
235
+ meta.content = twitter.title;
236
+ }
237
+ if (twitter.description) {
238
+ const meta = getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" });
239
+ meta.content = twitter.description;
240
+ }
241
+ if (twitter.image) {
242
+ const meta = getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" });
243
+ meta.content = twitter.image;
244
+ }
245
+ if (twitter.imageAlt) {
246
+ const meta = getOrCreateMeta('meta[name="twitter:image:alt"]', { name: "twitter:image:alt" });
247
+ meta.content = twitter.imageAlt;
248
+ }
249
+ if (twitter.site) {
250
+ const meta = getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" });
251
+ meta.content = twitter.site;
252
+ }
253
+ if (twitter.creator) {
254
+ const meta = getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" });
255
+ meta.content = twitter.creator;
256
+ }
257
+ }
258
+ if (md.metaTags && Array.isArray(md.metaTags)) {
259
+ md.metaTags.forEach((tag) => {
260
+ let selector = "";
261
+ if (tag.name) {
262
+ selector = `meta[name="${tag.name}"]`;
263
+ } else if (tag.property) {
264
+ selector = `meta[property="${tag.property}"]`;
265
+ } else if (tag.httpEquiv) {
266
+ selector = `meta[http-equiv="${tag.httpEquiv}"]`;
267
+ }
268
+ if (selector) {
269
+ const meta = getOrCreateMeta(selector, {
270
+ name: tag.name,
271
+ property: tag.property,
272
+ httpEquiv: tag.httpEquiv
273
+ });
274
+ meta.content = tag.content;
275
+ }
276
+ });
277
+ }
278
+ if (md.links && Array.isArray(md.links)) {
279
+ md.links.forEach((link) => {
280
+ getOrCreateLink(link.rel, link.href);
281
+ });
282
+ }
112
283
  }
113
284
 
114
285
  // modules/runtime/client/AppShell.tsx
@@ -221,14 +392,16 @@ function deleteCacheEntry(key) {
221
392
  function buildDataUrl(url) {
222
393
  return url + (url.includes("?") ? "&" : "?") + "__fw_data=1";
223
394
  }
224
- async function fetchRouteDataOnce(url) {
395
+ async function fetchRouteDataOnce(url, skipLayoutHooks = true) {
225
396
  const dataUrl = buildDataUrl(url);
226
- const res = await fetch(dataUrl, {
227
- headers: {
228
- "x-fw-data": "1",
229
- Accept: "application/json"
230
- }
231
- });
397
+ const headers = {
398
+ "x-fw-data": "1",
399
+ Accept: "application/json"
400
+ };
401
+ if (skipLayoutHooks) {
402
+ headers["x-skip-layout-hooks"] = "true";
403
+ }
404
+ const res = await fetch(dataUrl, { headers });
232
405
  let json = {};
233
406
  try {
234
407
  const text = await res.text();
@@ -254,7 +427,7 @@ async function getRouteData(url, options) {
254
427
  deleteCacheEntry(key);
255
428
  }
256
429
  const entry = dataCache.get(key);
257
- if (entry) {
430
+ if (entry && !options?.revalidate) {
258
431
  if (entry.status === "fulfilled") {
259
432
  updateLRU(key);
260
433
  return entry.value;
@@ -263,12 +436,29 @@ async function getRouteData(url, options) {
263
436
  return entry.promise;
264
437
  }
265
438
  }
266
- const promise = fetchRouteDataOnce(url).then((value) => {
267
- setCacheEntry(key, { status: "fulfilled", value });
439
+ const skipLayoutHooks = !options?.revalidate;
440
+ const currentEntry = dataCache.get(key);
441
+ if (currentEntry && !options?.revalidate) {
442
+ if (currentEntry.status === "fulfilled") {
443
+ updateLRU(key);
444
+ return currentEntry.value;
445
+ }
446
+ if (currentEntry.status === "pending") {
447
+ return currentEntry.promise;
448
+ }
449
+ }
450
+ const promise = fetchRouteDataOnce(url, skipLayoutHooks).then((value) => {
451
+ const entryAfterFetch = dataCache.get(key);
452
+ if (!entryAfterFetch || entryAfterFetch.status === "pending") {
453
+ setCacheEntry(key, { status: "fulfilled", value });
454
+ }
268
455
  return value;
269
456
  }).catch((error) => {
270
457
  console.error("[client][cache] Error fetching route data:", error);
271
- dataCache.set(key, { status: "rejected", error });
458
+ const entryAfterFetch = dataCache.get(key);
459
+ if (!entryAfterFetch || entryAfterFetch.status === "pending") {
460
+ dataCache.set(key, { status: "rejected", error });
461
+ }
272
462
  throw error;
273
463
  });
274
464
  dataCache.set(key, { status: "pending", promise });
@@ -293,10 +483,22 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
293
483
  } else if (json.theme) {
294
484
  theme = json.theme;
295
485
  }
486
+ let layoutProps = {};
487
+ if (json.layoutProps !== void 0 && json.layoutProps !== null) {
488
+ layoutProps = json.layoutProps;
489
+ setPreservedLayoutProps(layoutProps);
490
+ } else {
491
+ const preserved = getPreservedLayoutProps();
492
+ if (preserved) {
493
+ layoutProps = preserved;
494
+ }
495
+ }
496
+ const pageProps = json.pageProps ?? json.props ?? {
497
+ error: json.message || "An error occurred"
498
+ };
296
499
  const errorProps = {
297
- ...json.props || {
298
- error: json.message || "An error occurred"
299
- },
500
+ ...layoutProps,
501
+ ...pageProps,
300
502
  theme
301
503
  };
302
504
  const windowData = {
@@ -309,6 +511,13 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
309
511
  error: true
310
512
  };
311
513
  setWindowData(windowData);
514
+ const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
515
+ const routerData = {
516
+ pathname: url.pathname,
517
+ params: json.params || {},
518
+ searchParams: Object.fromEntries(url.searchParams.entries())
519
+ };
520
+ setRouterData(routerData);
312
521
  setState({
313
522
  url: nextUrl,
314
523
  route: errorRoute,
@@ -318,10 +527,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
318
527
  });
319
528
  return true;
320
529
  } catch (loadError) {
321
- console.error(
322
- "[client] Error loading error route components:",
323
- loadError
324
- );
530
+ console.error("\n\u274C [client] Error loading error route components:");
531
+ console.error(loadError);
532
+ if (loadError instanceof Error) {
533
+ console.error(` Message: ${loadError.message}`);
534
+ if (loadError.stack) {
535
+ console.error(` Stack: ${loadError.stack.split("\n").slice(0, 3).join("\n ")}`);
536
+ }
537
+ }
538
+ console.error("\u{1F4A1} Falling back to full page reload\n");
325
539
  window.location.href = nextUrl;
326
540
  return false;
327
541
  }
@@ -341,8 +555,20 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
341
555
  } else if (json.theme) {
342
556
  theme = json.theme;
343
557
  }
558
+ let layoutProps = {};
559
+ if (json.layoutProps !== void 0 && json.layoutProps !== null) {
560
+ layoutProps = json.layoutProps;
561
+ setPreservedLayoutProps(layoutProps);
562
+ } else {
563
+ const preserved = getPreservedLayoutProps();
564
+ if (preserved) {
565
+ layoutProps = preserved;
566
+ }
567
+ }
568
+ const pageProps = json.pageProps ?? json.props ?? {};
344
569
  const notFoundProps = {
345
- ...json.props ?? {},
570
+ ...layoutProps,
571
+ ...pageProps,
346
572
  theme
347
573
  };
348
574
  const windowData = {
@@ -355,6 +581,13 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
355
581
  error: false
356
582
  };
357
583
  setWindowData(windowData);
584
+ const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
585
+ const routerData = {
586
+ pathname: url.pathname,
587
+ params: {},
588
+ searchParams: Object.fromEntries(url.searchParams.entries())
589
+ };
590
+ setRouterData(routerData);
358
591
  if (notFoundRoute) {
359
592
  const components = await notFoundRoute.load();
360
593
  setState({
@@ -392,27 +625,59 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
392
625
  } else if (json.theme) {
393
626
  theme = json.theme;
394
627
  }
395
- const newProps = {
396
- ...json.props ?? {},
628
+ let layoutProps = {};
629
+ if (json.layoutProps !== void 0 && json.layoutProps !== null) {
630
+ layoutProps = json.layoutProps;
631
+ setPreservedLayoutProps(layoutProps);
632
+ } else {
633
+ const preserved = getPreservedLayoutProps();
634
+ if (preserved) {
635
+ layoutProps = preserved;
636
+ }
637
+ }
638
+ const pageProps = json.pageProps ?? json.props ?? {};
639
+ const combinedProps = {
640
+ ...layoutProps,
641
+ ...pageProps,
397
642
  theme
398
643
  // Always include theme
399
644
  };
400
- const matched = matchRouteClient(nextUrl, routes);
645
+ const pathnameForMatch = json.pathname || nextUrl;
646
+ let matched = matchRouteClient(pathnameForMatch, routes);
401
647
  if (!matched) {
648
+ matched = matchRouteClient(nextUrl, routes);
649
+ }
650
+ if (!matched) {
651
+ console.warn(
652
+ `[client] Server returned data for ${nextUrl} but no route match found. Available routes:`,
653
+ routes.map((r) => r.pattern)
654
+ );
402
655
  window.location.href = nextUrl;
403
656
  return false;
404
657
  }
658
+ const finalPathname = json.pathname || nextUrl;
405
659
  const windowData = {
406
- pathname: nextUrl,
660
+ pathname: finalPathname,
407
661
  params: matched.params,
408
- props: newProps,
662
+ props: combinedProps,
409
663
  metadata: json.metadata ?? null,
410
664
  theme,
411
665
  notFound: false,
412
666
  error: false
413
667
  };
414
668
  setWindowData(windowData);
415
- const components = await matched.route.load();
669
+ const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
670
+ const routerData = {
671
+ pathname: url.pathname,
672
+ params: matched.params,
673
+ searchParams: Object.fromEntries(url.searchParams.entries())
674
+ };
675
+ setRouterData(routerData);
676
+ const prefetched = prefetchedRoutes.get(matched.route);
677
+ const components = prefetched ? await prefetched : await matched.route.load();
678
+ if (!prefetched) {
679
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
680
+ }
416
681
  window.scrollTo({
417
682
  top: 0,
418
683
  behavior: "smooth"
@@ -422,7 +687,7 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
422
687
  route: matched.route,
423
688
  params: matched.params,
424
689
  components,
425
- props: newProps
690
+ props: combinedProps
426
691
  });
427
692
  return true;
428
693
  }
@@ -451,7 +716,7 @@ async function navigate(nextUrl, handlers, options) {
451
716
  }
452
717
  }
453
718
  if (!ok) {
454
- if (json && json.redirect) {
719
+ if (json?.redirect) {
455
720
  window.location.href = json.redirect.destination;
456
721
  return;
457
722
  }
@@ -472,6 +737,47 @@ async function navigate(nextUrl, handlers, options) {
472
737
  window.location.href = nextUrl;
473
738
  }
474
739
  }
740
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
741
+ function prefetchRoute(url, routes, notFoundRoute) {
742
+ const [pathname] = url.split("?");
743
+ const matched = matchRouteClient(pathname, routes);
744
+ if (!matched) {
745
+ if (notFoundRoute) {
746
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
747
+ if (!existing2) {
748
+ const promise = notFoundRoute.load();
749
+ prefetchedRoutes.set(notFoundRoute, promise);
750
+ }
751
+ }
752
+ return;
753
+ }
754
+ const existing = prefetchedRoutes.get(matched.route);
755
+ if (!existing) {
756
+ const promise = matched.route.load();
757
+ prefetchedRoutes.set(matched.route, promise);
758
+ }
759
+ }
760
+ function createHoverHandler(routes, notFoundRoute) {
761
+ return function handleHover(ev) {
762
+ try {
763
+ const target = ev.target;
764
+ if (!target) return;
765
+ const anchor = target.closest("a[href]");
766
+ if (!anchor) return;
767
+ const href = anchor.getAttribute("href");
768
+ if (!href) return;
769
+ if (href.startsWith("#")) return;
770
+ const url = new URL(href, window.location.href);
771
+ if (url.origin !== window.location.origin) return;
772
+ if (anchor.target && anchor.target !== "_self") return;
773
+ const nextUrl = url.pathname + url.search;
774
+ const currentUrl = window.location.pathname + window.location.search;
775
+ if (nextUrl === currentUrl) return;
776
+ prefetchRoute(nextUrl, routes, notFoundRoute);
777
+ } catch (error) {
778
+ }
779
+ };
780
+ }
475
781
  function createClickHandler(navigate2) {
476
782
  return function handleClick(ev) {
477
783
  try {
@@ -566,6 +872,14 @@ function AppShell({
566
872
  },
567
873
  []
568
874
  );
875
+ (0, import_react2.useEffect)(() => {
876
+ if (typeof window !== "undefined") {
877
+ window[ROUTER_NAVIGATE_KEY] = handleNavigate;
878
+ return () => {
879
+ delete window[ROUTER_NAVIGATE_KEY];
880
+ };
881
+ }
882
+ }, [handleNavigate]);
569
883
  (0, import_react2.useEffect)(() => {
570
884
  let isMounted = true;
571
885
  async function handleNavigateInternal(nextUrl, options) {
@@ -574,12 +888,37 @@ function AppShell({
574
888
  }
575
889
  const handleClick = createClickHandler(handleNavigateInternal);
576
890
  const handlePopState = createPopStateHandler(handleNavigateInternal);
891
+ const handleHover = createHoverHandler(routes, notFoundRoute);
577
892
  window.addEventListener("click", handleClick, false);
578
893
  window.addEventListener("popstate", handlePopState, false);
894
+ window.addEventListener("mouseover", handleHover, false);
579
895
  return () => {
580
896
  isMounted = false;
581
897
  window.removeEventListener("click", handleClick, false);
582
898
  window.removeEventListener("popstate", handlePopState, false);
899
+ window.removeEventListener("mouseover", handleHover, false);
900
+ };
901
+ }, [routes, notFoundRoute]);
902
+ (0, import_react2.useEffect)(() => {
903
+ const handleDataRefresh = () => {
904
+ const freshData = window[WINDOW_DATA_KEY];
905
+ if (!freshData) return;
906
+ const currentPathname = window.location.pathname;
907
+ const freshPathname = freshData.pathname;
908
+ if (freshPathname === currentPathname) {
909
+ if (freshData.metadata !== void 0) {
910
+ applyMetadata(freshData.metadata);
911
+ }
912
+ setState((prevState) => ({
913
+ ...prevState,
914
+ props: freshData.props ?? prevState.props,
915
+ params: freshData.params ?? prevState.params
916
+ }));
917
+ }
918
+ };
919
+ window.addEventListener("fw-data-refresh", handleDataRefresh);
920
+ return () => {
921
+ window.removeEventListener("fw-data-refresh", handleDataRefresh);
583
922
  };
584
923
  }, []);
585
924
  const isError = state.route === errorRoute;
@@ -589,6 +928,89 @@ function AppShell({
589
928
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterView, { state }, routeKey) });
590
929
  }
591
930
 
931
+ // modules/runtime/client/hot-reload.ts
932
+ function setupHotReload() {
933
+ const nodeEnv = process.env.NODE_ENV || "production";
934
+ const isDev = nodeEnv === "development";
935
+ if (!isDev) {
936
+ return;
937
+ }
938
+ console.log("[hot-reload] Setting up hot reload client...");
939
+ let eventSource = null;
940
+ let reloadTimeout = null;
941
+ let reconnectTimeout = null;
942
+ let reconnectAttempts = 0;
943
+ const MAX_RECONNECT_ATTEMPTS = 10;
944
+ const RECONNECT_DELAY = 1e3;
945
+ const RELOAD_DELAY = 100;
946
+ function connect() {
947
+ try {
948
+ if (eventSource) {
949
+ console.log("[hot-reload] Closing existing EventSource connection");
950
+ eventSource.close();
951
+ }
952
+ const endpoint = "/__fw/hot";
953
+ eventSource = new EventSource(endpoint);
954
+ eventSource.addEventListener("ping", (event) => {
955
+ if ("data" in event) {
956
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
957
+ }
958
+ reconnectAttempts = 0;
959
+ });
960
+ eventSource.addEventListener("message", (event) => {
961
+ const data = event.data;
962
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
963
+ const filePath = data.slice(7);
964
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
965
+ if (reloadTimeout) {
966
+ clearTimeout(reloadTimeout);
967
+ }
968
+ reloadTimeout = setTimeout(() => {
969
+ try {
970
+ window.location.reload();
971
+ } catch (error) {
972
+ console.error("[hot-reload] \u274C Error reloading page:", error);
973
+ setTimeout(() => window.location.reload(), 100);
974
+ }
975
+ }, RELOAD_DELAY);
976
+ }
977
+ });
978
+ eventSource.onopen = () => {
979
+ reconnectAttempts = 0;
980
+ };
981
+ eventSource.onerror = (error) => {
982
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
983
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
984
+ if (eventSource?.readyState === EventSource.CONNECTING) {
985
+ console.log("[hot-reload] \u23F3 Still connecting...");
986
+ return;
987
+ } else if (eventSource?.readyState === EventSource.OPEN) {
988
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
989
+ } else {
990
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
991
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
992
+ reconnectAttempts++;
993
+ const delay = RECONNECT_DELAY * reconnectAttempts;
994
+ if (reconnectTimeout) {
995
+ clearTimeout(reconnectTimeout);
996
+ }
997
+ reconnectTimeout = setTimeout(() => {
998
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
999
+ connect();
1000
+ }, delay);
1001
+ } else {
1002
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
1003
+ }
1004
+ }
1005
+ };
1006
+ } catch (error) {
1007
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
1008
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
1009
+ }
1010
+ }
1011
+ connect();
1012
+ }
1013
+
592
1014
  // modules/runtime/client/bootstrap.tsx
593
1015
  var import_jsx_runtime3 = require("react/jsx-runtime");
594
1016
  async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute, errorRoute) {
@@ -630,91 +1052,96 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
630
1052
  props: initialData?.props ?? {}
631
1053
  };
632
1054
  }
633
- function setupHotReload() {
634
- const nodeEnv = typeof process !== "undefined" && process?.env?.NODE_ENV || "production";
635
- const isDev = nodeEnv !== "production";
636
- if (!isDev) {
637
- return;
1055
+ function initializeRouterData(initialUrl, initialData) {
1056
+ let routerData = getRouterData();
1057
+ if (!routerData) {
1058
+ const url = new URL(initialUrl, window.location.origin);
1059
+ const pathname = initialData?.pathname || url.pathname;
1060
+ routerData = {
1061
+ pathname,
1062
+ params: initialData?.params || {},
1063
+ searchParams: Object.fromEntries(url.searchParams.entries())
1064
+ };
1065
+ setRouterData(routerData);
638
1066
  }
1067
+ }
1068
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
639
1069
  try {
640
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
641
- const eventSource = new EventSource("/__fw/hot");
642
- let reloadTimeout = null;
643
- eventSource.addEventListener("message", (event) => {
644
- const data = event.data;
645
- if (data && data.startsWith("reload:")) {
646
- const filePath = data.slice(7);
647
- console.log(`[hot-reload] File changed: ${filePath}`);
648
- if (reloadTimeout) {
649
- clearTimeout(reloadTimeout);
650
- }
651
- reloadTimeout = setTimeout(() => {
652
- console.log("[hot-reload] Reloading page...");
653
- window.location.reload();
654
- }, 500);
655
- }
656
- });
657
- eventSource.addEventListener("ping", () => {
658
- console.log("[hot-reload] \u2713 Connected to hot reload server");
659
- });
660
- eventSource.onopen = () => {
661
- console.log("[hot-reload] \u2713 SSE connection opened");
662
- };
663
- eventSource.onerror = (error) => {
664
- const states = ["CONNECTING", "OPEN", "CLOSED"];
665
- const state = states[eventSource.readyState] || "UNKNOWN";
666
- if (eventSource.readyState === EventSource.CONNECTING) {
667
- console.log("[hot-reload] Connecting...");
668
- } else if (eventSource.readyState === EventSource.OPEN) {
669
- console.warn("[hot-reload] Connection error (but connection is open):", error);
670
- } else {
671
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
1070
+ const initialState = await loadInitialRoute(
1071
+ initialUrl,
1072
+ initialData,
1073
+ routes,
1074
+ notFoundRoute,
1075
+ errorRoute
1076
+ );
1077
+ if (initialData?.metadata) {
1078
+ try {
1079
+ applyMetadata(initialData.metadata);
1080
+ } catch (metadataError) {
1081
+ console.warn("[client] Error applying metadata:", metadataError);
672
1082
  }
673
- };
1083
+ }
1084
+ (0, import_client.hydrateRoot)(
1085
+ container,
1086
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1087
+ AppShell,
1088
+ {
1089
+ initialState,
1090
+ routes,
1091
+ notFoundRoute,
1092
+ errorRoute
1093
+ }
1094
+ )
1095
+ );
674
1096
  } catch (error) {
675
- console.log("[hot-reload] EventSource not supported or error:", error);
1097
+ console.error(
1098
+ "[client] Error loading initial route components for",
1099
+ initialUrl,
1100
+ error
1101
+ );
1102
+ throw error;
676
1103
  }
677
1104
  }
678
1105
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
679
- console.log("[client] Bootstrap starting, setting up hot reload...");
680
1106
  setupHotReload();
681
- (async function bootstrap() {
682
- const container = document.getElementById(APP_CONTAINER_ID);
683
- const initialData = getWindowData();
684
- if (!container) {
685
- console.error(`Container #${APP_CONTAINER_ID} not found for hydration`);
686
- return;
687
- }
688
- const initialUrl = window.location.pathname + window.location.search;
1107
+ (async () => {
689
1108
  try {
690
- const initialState = await loadInitialRoute(
1109
+ const container = document.getElementById(APP_CONTAINER_ID);
1110
+ if (!container) {
1111
+ console.error(`
1112
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID} not found`);
1113
+ console.error("\u{1F4A1} This usually means:");
1114
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
1115
+ console.error(" \u2022 The container was removed before hydration");
1116
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
1117
+ return;
1118
+ }
1119
+ const initialData = getWindowData();
1120
+ const initialUrl = (initialData?.pathname || window.location.pathname) + window.location.search;
1121
+ if (initialData?.props) {
1122
+ setPreservedLayoutProps(initialData.props);
1123
+ }
1124
+ const routerPathname = initialData?.pathname || window.location.pathname;
1125
+ initializeRouterData(routerPathname + window.location.search, initialData);
1126
+ await hydrateInitialRoute(
1127
+ container,
691
1128
  initialUrl,
692
1129
  initialData,
693
1130
  routes,
694
1131
  notFoundRoute,
695
1132
  errorRoute
696
1133
  );
697
- if (initialData?.metadata) {
698
- applyMetadata(initialData.metadata);
699
- }
700
- (0, import_client.hydrateRoot)(
701
- container,
702
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
703
- AppShell,
704
- {
705
- initialState,
706
- routes,
707
- notFoundRoute,
708
- errorRoute
709
- }
710
- )
711
- );
712
1134
  } catch (error) {
713
- console.error(
714
- "[client] Error loading initial route components for",
715
- initialUrl,
716
- error
717
- );
1135
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
1136
+ console.error(error);
1137
+ if (error instanceof Error) {
1138
+ console.error("\nError details:");
1139
+ console.error(` Message: ${error.message}`);
1140
+ if (error.stack) {
1141
+ console.error(` Stack: ${error.stack}`);
1142
+ }
1143
+ }
1144
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
718
1145
  window.location.reload();
719
1146
  }
720
1147
  })();