@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.js CHANGED
@@ -3,15 +3,40 @@ import { hydrateRoot } from "react-dom/client";
3
3
 
4
4
  // modules/runtime/client/constants.ts
5
5
  var WINDOW_DATA_KEY = "__FW_DATA__";
6
+ var ROUTER_DATA_KEY = "__LOLY_ROUTER_DATA__";
6
7
  var APP_CONTAINER_ID = "__app";
8
+ var ROUTER_NAVIGATE_KEY = "__LOLY_ROUTER_NAVIGATE__";
7
9
 
8
10
  // modules/runtime/client/window-data.ts
11
+ var LAYOUT_PROPS_KEY = "__FW_LAYOUT_PROPS__";
9
12
  function getWindowData() {
10
13
  if (typeof window === "undefined") {
11
14
  return null;
12
15
  }
13
16
  return window[WINDOW_DATA_KEY] ?? null;
14
17
  }
18
+ function getPreservedLayoutProps() {
19
+ if (typeof window === "undefined") {
20
+ return null;
21
+ }
22
+ return window[LAYOUT_PROPS_KEY] ?? null;
23
+ }
24
+ function setPreservedLayoutProps(props) {
25
+ if (typeof window === "undefined") {
26
+ return;
27
+ }
28
+ if (props === null) {
29
+ delete window[LAYOUT_PROPS_KEY];
30
+ } else {
31
+ window[LAYOUT_PROPS_KEY] = props;
32
+ }
33
+ }
34
+ function getRouterData() {
35
+ if (typeof window === "undefined") {
36
+ return null;
37
+ }
38
+ return window[ROUTER_DATA_KEY] ?? null;
39
+ }
15
40
  function setWindowData(data) {
16
41
  window[WINDOW_DATA_KEY] = data;
17
42
  if (typeof window !== "undefined") {
@@ -22,6 +47,16 @@ function setWindowData(data) {
22
47
  );
23
48
  }
24
49
  }
50
+ function setRouterData(data) {
51
+ window[ROUTER_DATA_KEY] = data;
52
+ if (typeof window !== "undefined") {
53
+ window.dispatchEvent(
54
+ new CustomEvent("fw-router-data-refresh", {
55
+ detail: { data }
56
+ })
57
+ );
58
+ }
59
+ }
25
60
  function getCurrentTheme() {
26
61
  return getWindowData()?.theme ?? null;
27
62
  }
@@ -67,22 +102,158 @@ function matchRouteClient(pathWithSearch, routes) {
67
102
  }
68
103
 
69
104
  // modules/runtime/client/metadata.ts
105
+ function getOrCreateMeta(selector, attributes) {
106
+ let meta = document.querySelector(selector);
107
+ if (!meta) {
108
+ meta = document.createElement("meta");
109
+ if (attributes.name) meta.name = attributes.name;
110
+ if (attributes.property) meta.setAttribute("property", attributes.property);
111
+ if (attributes.httpEquiv) meta.httpEquiv = attributes.httpEquiv;
112
+ document.head.appendChild(meta);
113
+ }
114
+ return meta;
115
+ }
116
+ function getOrCreateLink(rel, href) {
117
+ const selector = `link[rel="${rel}"]`;
118
+ let link = document.querySelector(selector);
119
+ if (!link) {
120
+ link = document.createElement("link");
121
+ link.rel = rel;
122
+ link.href = href;
123
+ document.head.appendChild(link);
124
+ } else {
125
+ link.href = href;
126
+ }
127
+ return link;
128
+ }
70
129
  function applyMetadata(md) {
71
130
  if (!md) return;
72
131
  if (md.title) {
73
132
  document.title = md.title;
74
133
  }
75
134
  if (md.description) {
76
- let meta = document.querySelector(
77
- 'meta[name="description"]'
78
- );
79
- if (!meta) {
80
- meta = document.createElement("meta");
81
- meta.name = "description";
82
- document.head.appendChild(meta);
83
- }
135
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
84
136
  meta.content = md.description;
85
137
  }
138
+ if (md.robots) {
139
+ const meta = getOrCreateMeta('meta[name="robots"]', { name: "robots" });
140
+ meta.content = md.robots;
141
+ }
142
+ if (md.themeColor) {
143
+ const meta = getOrCreateMeta('meta[name="theme-color"]', { name: "theme-color" });
144
+ meta.content = md.themeColor;
145
+ }
146
+ if (md.viewport) {
147
+ const meta = getOrCreateMeta('meta[name="viewport"]', { name: "viewport" });
148
+ meta.content = md.viewport;
149
+ }
150
+ if (md.canonical) {
151
+ getOrCreateLink("canonical", md.canonical);
152
+ }
153
+ if (md.openGraph) {
154
+ const og = md.openGraph;
155
+ if (og.title) {
156
+ const meta = getOrCreateMeta('meta[property="og:title"]', { property: "og:title" });
157
+ meta.content = og.title;
158
+ }
159
+ if (og.description) {
160
+ const meta = getOrCreateMeta('meta[property="og:description"]', { property: "og:description" });
161
+ meta.content = og.description;
162
+ }
163
+ if (og.type) {
164
+ const meta = getOrCreateMeta('meta[property="og:type"]', { property: "og:type" });
165
+ meta.content = og.type;
166
+ }
167
+ if (og.url) {
168
+ const meta = getOrCreateMeta('meta[property="og:url"]', { property: "og:url" });
169
+ meta.content = og.url;
170
+ }
171
+ if (og.image) {
172
+ if (typeof og.image === "string") {
173
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
174
+ meta.content = og.image;
175
+ } else {
176
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
177
+ meta.content = og.image.url;
178
+ if (og.image.width) {
179
+ const metaWidth = getOrCreateMeta('meta[property="og:image:width"]', { property: "og:image:width" });
180
+ metaWidth.content = String(og.image.width);
181
+ }
182
+ if (og.image.height) {
183
+ const metaHeight = getOrCreateMeta('meta[property="og:image:height"]', { property: "og:image:height" });
184
+ metaHeight.content = String(og.image.height);
185
+ }
186
+ if (og.image.alt) {
187
+ const metaAlt = getOrCreateMeta('meta[property="og:image:alt"]', { property: "og:image:alt" });
188
+ metaAlt.content = og.image.alt;
189
+ }
190
+ }
191
+ }
192
+ if (og.siteName) {
193
+ const meta = getOrCreateMeta('meta[property="og:site_name"]', { property: "og:site_name" });
194
+ meta.content = og.siteName;
195
+ }
196
+ if (og.locale) {
197
+ const meta = getOrCreateMeta('meta[property="og:locale"]', { property: "og:locale" });
198
+ meta.content = og.locale;
199
+ }
200
+ }
201
+ if (md.twitter) {
202
+ const twitter = md.twitter;
203
+ if (twitter.card) {
204
+ const meta = getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" });
205
+ meta.content = twitter.card;
206
+ }
207
+ if (twitter.title) {
208
+ const meta = getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" });
209
+ meta.content = twitter.title;
210
+ }
211
+ if (twitter.description) {
212
+ const meta = getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" });
213
+ meta.content = twitter.description;
214
+ }
215
+ if (twitter.image) {
216
+ const meta = getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" });
217
+ meta.content = twitter.image;
218
+ }
219
+ if (twitter.imageAlt) {
220
+ const meta = getOrCreateMeta('meta[name="twitter:image:alt"]', { name: "twitter:image:alt" });
221
+ meta.content = twitter.imageAlt;
222
+ }
223
+ if (twitter.site) {
224
+ const meta = getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" });
225
+ meta.content = twitter.site;
226
+ }
227
+ if (twitter.creator) {
228
+ const meta = getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" });
229
+ meta.content = twitter.creator;
230
+ }
231
+ }
232
+ if (md.metaTags && Array.isArray(md.metaTags)) {
233
+ md.metaTags.forEach((tag) => {
234
+ let selector = "";
235
+ if (tag.name) {
236
+ selector = `meta[name="${tag.name}"]`;
237
+ } else if (tag.property) {
238
+ selector = `meta[property="${tag.property}"]`;
239
+ } else if (tag.httpEquiv) {
240
+ selector = `meta[http-equiv="${tag.httpEquiv}"]`;
241
+ }
242
+ if (selector) {
243
+ const meta = getOrCreateMeta(selector, {
244
+ name: tag.name,
245
+ property: tag.property,
246
+ httpEquiv: tag.httpEquiv
247
+ });
248
+ meta.content = tag.content;
249
+ }
250
+ });
251
+ }
252
+ if (md.links && Array.isArray(md.links)) {
253
+ md.links.forEach((link) => {
254
+ getOrCreateLink(link.rel, link.href);
255
+ });
256
+ }
86
257
  }
87
258
 
88
259
  // modules/runtime/client/AppShell.tsx
@@ -195,14 +366,16 @@ function deleteCacheEntry(key) {
195
366
  function buildDataUrl(url) {
196
367
  return url + (url.includes("?") ? "&" : "?") + "__fw_data=1";
197
368
  }
198
- async function fetchRouteDataOnce(url) {
369
+ async function fetchRouteDataOnce(url, skipLayoutHooks = true) {
199
370
  const dataUrl = buildDataUrl(url);
200
- const res = await fetch(dataUrl, {
201
- headers: {
202
- "x-fw-data": "1",
203
- Accept: "application/json"
204
- }
205
- });
371
+ const headers = {
372
+ "x-fw-data": "1",
373
+ Accept: "application/json"
374
+ };
375
+ if (skipLayoutHooks) {
376
+ headers["x-skip-layout-hooks"] = "true";
377
+ }
378
+ const res = await fetch(dataUrl, { headers });
206
379
  let json = {};
207
380
  try {
208
381
  const text = await res.text();
@@ -228,7 +401,7 @@ async function getRouteData(url, options) {
228
401
  deleteCacheEntry(key);
229
402
  }
230
403
  const entry = dataCache.get(key);
231
- if (entry) {
404
+ if (entry && !options?.revalidate) {
232
405
  if (entry.status === "fulfilled") {
233
406
  updateLRU(key);
234
407
  return entry.value;
@@ -237,12 +410,29 @@ async function getRouteData(url, options) {
237
410
  return entry.promise;
238
411
  }
239
412
  }
240
- const promise = fetchRouteDataOnce(url).then((value) => {
241
- setCacheEntry(key, { status: "fulfilled", value });
413
+ const skipLayoutHooks = !options?.revalidate;
414
+ const currentEntry = dataCache.get(key);
415
+ if (currentEntry && !options?.revalidate) {
416
+ if (currentEntry.status === "fulfilled") {
417
+ updateLRU(key);
418
+ return currentEntry.value;
419
+ }
420
+ if (currentEntry.status === "pending") {
421
+ return currentEntry.promise;
422
+ }
423
+ }
424
+ const promise = fetchRouteDataOnce(url, skipLayoutHooks).then((value) => {
425
+ const entryAfterFetch = dataCache.get(key);
426
+ if (!entryAfterFetch || entryAfterFetch.status === "pending") {
427
+ setCacheEntry(key, { status: "fulfilled", value });
428
+ }
242
429
  return value;
243
430
  }).catch((error) => {
244
431
  console.error("[client][cache] Error fetching route data:", error);
245
- dataCache.set(key, { status: "rejected", error });
432
+ const entryAfterFetch = dataCache.get(key);
433
+ if (!entryAfterFetch || entryAfterFetch.status === "pending") {
434
+ dataCache.set(key, { status: "rejected", error });
435
+ }
246
436
  throw error;
247
437
  });
248
438
  dataCache.set(key, { status: "pending", promise });
@@ -267,10 +457,22 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
267
457
  } else if (json.theme) {
268
458
  theme = json.theme;
269
459
  }
460
+ let layoutProps = {};
461
+ if (json.layoutProps !== void 0 && json.layoutProps !== null) {
462
+ layoutProps = json.layoutProps;
463
+ setPreservedLayoutProps(layoutProps);
464
+ } else {
465
+ const preserved = getPreservedLayoutProps();
466
+ if (preserved) {
467
+ layoutProps = preserved;
468
+ }
469
+ }
470
+ const pageProps = json.pageProps ?? json.props ?? {
471
+ error: json.message || "An error occurred"
472
+ };
270
473
  const errorProps = {
271
- ...json.props || {
272
- error: json.message || "An error occurred"
273
- },
474
+ ...layoutProps,
475
+ ...pageProps,
274
476
  theme
275
477
  };
276
478
  const windowData = {
@@ -283,6 +485,13 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
283
485
  error: true
284
486
  };
285
487
  setWindowData(windowData);
488
+ const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
489
+ const routerData = {
490
+ pathname: url.pathname,
491
+ params: json.params || {},
492
+ searchParams: Object.fromEntries(url.searchParams.entries())
493
+ };
494
+ setRouterData(routerData);
286
495
  setState({
287
496
  url: nextUrl,
288
497
  route: errorRoute,
@@ -292,10 +501,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
292
501
  });
293
502
  return true;
294
503
  } catch (loadError) {
295
- console.error(
296
- "[client] Error loading error route components:",
297
- loadError
298
- );
504
+ console.error("\n\u274C [client] Error loading error route components:");
505
+ console.error(loadError);
506
+ if (loadError instanceof Error) {
507
+ console.error(` Message: ${loadError.message}`);
508
+ if (loadError.stack) {
509
+ console.error(` Stack: ${loadError.stack.split("\n").slice(0, 3).join("\n ")}`);
510
+ }
511
+ }
512
+ console.error("\u{1F4A1} Falling back to full page reload\n");
299
513
  window.location.href = nextUrl;
300
514
  return false;
301
515
  }
@@ -315,8 +529,20 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
315
529
  } else if (json.theme) {
316
530
  theme = json.theme;
317
531
  }
532
+ let layoutProps = {};
533
+ if (json.layoutProps !== void 0 && json.layoutProps !== null) {
534
+ layoutProps = json.layoutProps;
535
+ setPreservedLayoutProps(layoutProps);
536
+ } else {
537
+ const preserved = getPreservedLayoutProps();
538
+ if (preserved) {
539
+ layoutProps = preserved;
540
+ }
541
+ }
542
+ const pageProps = json.pageProps ?? json.props ?? {};
318
543
  const notFoundProps = {
319
- ...json.props ?? {},
544
+ ...layoutProps,
545
+ ...pageProps,
320
546
  theme
321
547
  };
322
548
  const windowData = {
@@ -329,6 +555,13 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
329
555
  error: false
330
556
  };
331
557
  setWindowData(windowData);
558
+ const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
559
+ const routerData = {
560
+ pathname: url.pathname,
561
+ params: {},
562
+ searchParams: Object.fromEntries(url.searchParams.entries())
563
+ };
564
+ setRouterData(routerData);
332
565
  if (notFoundRoute) {
333
566
  const components = await notFoundRoute.load();
334
567
  setState({
@@ -366,27 +599,59 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
366
599
  } else if (json.theme) {
367
600
  theme = json.theme;
368
601
  }
369
- const newProps = {
370
- ...json.props ?? {},
602
+ let layoutProps = {};
603
+ if (json.layoutProps !== void 0 && json.layoutProps !== null) {
604
+ layoutProps = json.layoutProps;
605
+ setPreservedLayoutProps(layoutProps);
606
+ } else {
607
+ const preserved = getPreservedLayoutProps();
608
+ if (preserved) {
609
+ layoutProps = preserved;
610
+ }
611
+ }
612
+ const pageProps = json.pageProps ?? json.props ?? {};
613
+ const combinedProps = {
614
+ ...layoutProps,
615
+ ...pageProps,
371
616
  theme
372
617
  // Always include theme
373
618
  };
374
- const matched = matchRouteClient(nextUrl, routes);
619
+ const pathnameForMatch = json.pathname || nextUrl;
620
+ let matched = matchRouteClient(pathnameForMatch, routes);
375
621
  if (!matched) {
622
+ matched = matchRouteClient(nextUrl, routes);
623
+ }
624
+ if (!matched) {
625
+ console.warn(
626
+ `[client] Server returned data for ${nextUrl} but no route match found. Available routes:`,
627
+ routes.map((r) => r.pattern)
628
+ );
376
629
  window.location.href = nextUrl;
377
630
  return false;
378
631
  }
632
+ const finalPathname = json.pathname || nextUrl;
379
633
  const windowData = {
380
- pathname: nextUrl,
634
+ pathname: finalPathname,
381
635
  params: matched.params,
382
- props: newProps,
636
+ props: combinedProps,
383
637
  metadata: json.metadata ?? null,
384
638
  theme,
385
639
  notFound: false,
386
640
  error: false
387
641
  };
388
642
  setWindowData(windowData);
389
- const components = await matched.route.load();
643
+ const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
644
+ const routerData = {
645
+ pathname: url.pathname,
646
+ params: matched.params,
647
+ searchParams: Object.fromEntries(url.searchParams.entries())
648
+ };
649
+ setRouterData(routerData);
650
+ const prefetched = prefetchedRoutes.get(matched.route);
651
+ const components = prefetched ? await prefetched : await matched.route.load();
652
+ if (!prefetched) {
653
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
654
+ }
390
655
  window.scrollTo({
391
656
  top: 0,
392
657
  behavior: "smooth"
@@ -396,7 +661,7 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
396
661
  route: matched.route,
397
662
  params: matched.params,
398
663
  components,
399
- props: newProps
664
+ props: combinedProps
400
665
  });
401
666
  return true;
402
667
  }
@@ -425,7 +690,7 @@ async function navigate(nextUrl, handlers, options) {
425
690
  }
426
691
  }
427
692
  if (!ok) {
428
- if (json && json.redirect) {
693
+ if (json?.redirect) {
429
694
  window.location.href = json.redirect.destination;
430
695
  return;
431
696
  }
@@ -446,6 +711,47 @@ async function navigate(nextUrl, handlers, options) {
446
711
  window.location.href = nextUrl;
447
712
  }
448
713
  }
714
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
715
+ function prefetchRoute(url, routes, notFoundRoute) {
716
+ const [pathname] = url.split("?");
717
+ const matched = matchRouteClient(pathname, routes);
718
+ if (!matched) {
719
+ if (notFoundRoute) {
720
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
721
+ if (!existing2) {
722
+ const promise = notFoundRoute.load();
723
+ prefetchedRoutes.set(notFoundRoute, promise);
724
+ }
725
+ }
726
+ return;
727
+ }
728
+ const existing = prefetchedRoutes.get(matched.route);
729
+ if (!existing) {
730
+ const promise = matched.route.load();
731
+ prefetchedRoutes.set(matched.route, promise);
732
+ }
733
+ }
734
+ function createHoverHandler(routes, notFoundRoute) {
735
+ return function handleHover(ev) {
736
+ try {
737
+ const target = ev.target;
738
+ if (!target) return;
739
+ const anchor = target.closest("a[href]");
740
+ if (!anchor) return;
741
+ const href = anchor.getAttribute("href");
742
+ if (!href) return;
743
+ if (href.startsWith("#")) return;
744
+ const url = new URL(href, window.location.href);
745
+ if (url.origin !== window.location.origin) return;
746
+ if (anchor.target && anchor.target !== "_self") return;
747
+ const nextUrl = url.pathname + url.search;
748
+ const currentUrl = window.location.pathname + window.location.search;
749
+ if (nextUrl === currentUrl) return;
750
+ prefetchRoute(nextUrl, routes, notFoundRoute);
751
+ } catch (error) {
752
+ }
753
+ };
754
+ }
449
755
  function createClickHandler(navigate2) {
450
756
  return function handleClick(ev) {
451
757
  try {
@@ -540,6 +846,14 @@ function AppShell({
540
846
  },
541
847
  []
542
848
  );
849
+ useEffect(() => {
850
+ if (typeof window !== "undefined") {
851
+ window[ROUTER_NAVIGATE_KEY] = handleNavigate;
852
+ return () => {
853
+ delete window[ROUTER_NAVIGATE_KEY];
854
+ };
855
+ }
856
+ }, [handleNavigate]);
543
857
  useEffect(() => {
544
858
  let isMounted = true;
545
859
  async function handleNavigateInternal(nextUrl, options) {
@@ -548,12 +862,37 @@ function AppShell({
548
862
  }
549
863
  const handleClick = createClickHandler(handleNavigateInternal);
550
864
  const handlePopState = createPopStateHandler(handleNavigateInternal);
865
+ const handleHover = createHoverHandler(routes, notFoundRoute);
551
866
  window.addEventListener("click", handleClick, false);
552
867
  window.addEventListener("popstate", handlePopState, false);
868
+ window.addEventListener("mouseover", handleHover, false);
553
869
  return () => {
554
870
  isMounted = false;
555
871
  window.removeEventListener("click", handleClick, false);
556
872
  window.removeEventListener("popstate", handlePopState, false);
873
+ window.removeEventListener("mouseover", handleHover, false);
874
+ };
875
+ }, [routes, notFoundRoute]);
876
+ useEffect(() => {
877
+ const handleDataRefresh = () => {
878
+ const freshData = window[WINDOW_DATA_KEY];
879
+ if (!freshData) return;
880
+ const currentPathname = window.location.pathname;
881
+ const freshPathname = freshData.pathname;
882
+ if (freshPathname === currentPathname) {
883
+ if (freshData.metadata !== void 0) {
884
+ applyMetadata(freshData.metadata);
885
+ }
886
+ setState((prevState) => ({
887
+ ...prevState,
888
+ props: freshData.props ?? prevState.props,
889
+ params: freshData.params ?? prevState.params
890
+ }));
891
+ }
892
+ };
893
+ window.addEventListener("fw-data-refresh", handleDataRefresh);
894
+ return () => {
895
+ window.removeEventListener("fw-data-refresh", handleDataRefresh);
557
896
  };
558
897
  }, []);
559
898
  const isError = state.route === errorRoute;
@@ -563,6 +902,89 @@ function AppShell({
563
902
  return /* @__PURE__ */ jsx2(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ jsx2(RouterView, { state }, routeKey) });
564
903
  }
565
904
 
905
+ // modules/runtime/client/hot-reload.ts
906
+ function setupHotReload() {
907
+ const nodeEnv = process.env.NODE_ENV || "production";
908
+ const isDev = nodeEnv === "development";
909
+ if (!isDev) {
910
+ return;
911
+ }
912
+ console.log("[hot-reload] Setting up hot reload client...");
913
+ let eventSource = null;
914
+ let reloadTimeout = null;
915
+ let reconnectTimeout = null;
916
+ let reconnectAttempts = 0;
917
+ const MAX_RECONNECT_ATTEMPTS = 10;
918
+ const RECONNECT_DELAY = 1e3;
919
+ const RELOAD_DELAY = 100;
920
+ function connect() {
921
+ try {
922
+ if (eventSource) {
923
+ console.log("[hot-reload] Closing existing EventSource connection");
924
+ eventSource.close();
925
+ }
926
+ const endpoint = "/__fw/hot";
927
+ eventSource = new EventSource(endpoint);
928
+ eventSource.addEventListener("ping", (event) => {
929
+ if ("data" in event) {
930
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
931
+ }
932
+ reconnectAttempts = 0;
933
+ });
934
+ eventSource.addEventListener("message", (event) => {
935
+ const data = event.data;
936
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
937
+ const filePath = data.slice(7);
938
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
939
+ if (reloadTimeout) {
940
+ clearTimeout(reloadTimeout);
941
+ }
942
+ reloadTimeout = setTimeout(() => {
943
+ try {
944
+ window.location.reload();
945
+ } catch (error) {
946
+ console.error("[hot-reload] \u274C Error reloading page:", error);
947
+ setTimeout(() => window.location.reload(), 100);
948
+ }
949
+ }, RELOAD_DELAY);
950
+ }
951
+ });
952
+ eventSource.onopen = () => {
953
+ reconnectAttempts = 0;
954
+ };
955
+ eventSource.onerror = (error) => {
956
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
957
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
958
+ if (eventSource?.readyState === EventSource.CONNECTING) {
959
+ console.log("[hot-reload] \u23F3 Still connecting...");
960
+ return;
961
+ } else if (eventSource?.readyState === EventSource.OPEN) {
962
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
963
+ } else {
964
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
965
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
966
+ reconnectAttempts++;
967
+ const delay = RECONNECT_DELAY * reconnectAttempts;
968
+ if (reconnectTimeout) {
969
+ clearTimeout(reconnectTimeout);
970
+ }
971
+ reconnectTimeout = setTimeout(() => {
972
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
973
+ connect();
974
+ }, delay);
975
+ } else {
976
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
977
+ }
978
+ }
979
+ };
980
+ } catch (error) {
981
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
982
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
983
+ }
984
+ }
985
+ connect();
986
+ }
987
+
566
988
  // modules/runtime/client/bootstrap.tsx
567
989
  import { jsx as jsx3 } from "react/jsx-runtime";
568
990
  async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute, errorRoute) {
@@ -604,91 +1026,96 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
604
1026
  props: initialData?.props ?? {}
605
1027
  };
606
1028
  }
607
- function setupHotReload() {
608
- const nodeEnv = typeof process !== "undefined" && process?.env?.NODE_ENV || "production";
609
- const isDev = nodeEnv !== "production";
610
- if (!isDev) {
611
- return;
1029
+ function initializeRouterData(initialUrl, initialData) {
1030
+ let routerData = getRouterData();
1031
+ if (!routerData) {
1032
+ const url = new URL(initialUrl, window.location.origin);
1033
+ const pathname = initialData?.pathname || url.pathname;
1034
+ routerData = {
1035
+ pathname,
1036
+ params: initialData?.params || {},
1037
+ searchParams: Object.fromEntries(url.searchParams.entries())
1038
+ };
1039
+ setRouterData(routerData);
612
1040
  }
1041
+ }
1042
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
613
1043
  try {
614
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
615
- const eventSource = new EventSource("/__fw/hot");
616
- let reloadTimeout = null;
617
- eventSource.addEventListener("message", (event) => {
618
- const data = event.data;
619
- if (data && data.startsWith("reload:")) {
620
- const filePath = data.slice(7);
621
- console.log(`[hot-reload] File changed: ${filePath}`);
622
- if (reloadTimeout) {
623
- clearTimeout(reloadTimeout);
624
- }
625
- reloadTimeout = setTimeout(() => {
626
- console.log("[hot-reload] Reloading page...");
627
- window.location.reload();
628
- }, 500);
629
- }
630
- });
631
- eventSource.addEventListener("ping", () => {
632
- console.log("[hot-reload] \u2713 Connected to hot reload server");
633
- });
634
- eventSource.onopen = () => {
635
- console.log("[hot-reload] \u2713 SSE connection opened");
636
- };
637
- eventSource.onerror = (error) => {
638
- const states = ["CONNECTING", "OPEN", "CLOSED"];
639
- const state = states[eventSource.readyState] || "UNKNOWN";
640
- if (eventSource.readyState === EventSource.CONNECTING) {
641
- console.log("[hot-reload] Connecting...");
642
- } else if (eventSource.readyState === EventSource.OPEN) {
643
- console.warn("[hot-reload] Connection error (but connection is open):", error);
644
- } else {
645
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
1044
+ const initialState = await loadInitialRoute(
1045
+ initialUrl,
1046
+ initialData,
1047
+ routes,
1048
+ notFoundRoute,
1049
+ errorRoute
1050
+ );
1051
+ if (initialData?.metadata) {
1052
+ try {
1053
+ applyMetadata(initialData.metadata);
1054
+ } catch (metadataError) {
1055
+ console.warn("[client] Error applying metadata:", metadataError);
646
1056
  }
647
- };
1057
+ }
1058
+ hydrateRoot(
1059
+ container,
1060
+ /* @__PURE__ */ jsx3(
1061
+ AppShell,
1062
+ {
1063
+ initialState,
1064
+ routes,
1065
+ notFoundRoute,
1066
+ errorRoute
1067
+ }
1068
+ )
1069
+ );
648
1070
  } catch (error) {
649
- console.log("[hot-reload] EventSource not supported or error:", error);
1071
+ console.error(
1072
+ "[client] Error loading initial route components for",
1073
+ initialUrl,
1074
+ error
1075
+ );
1076
+ throw error;
650
1077
  }
651
1078
  }
652
1079
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
653
- console.log("[client] Bootstrap starting, setting up hot reload...");
654
1080
  setupHotReload();
655
- (async function bootstrap() {
656
- const container = document.getElementById(APP_CONTAINER_ID);
657
- const initialData = getWindowData();
658
- if (!container) {
659
- console.error(`Container #${APP_CONTAINER_ID} not found for hydration`);
660
- return;
661
- }
662
- const initialUrl = window.location.pathname + window.location.search;
1081
+ (async () => {
663
1082
  try {
664
- const initialState = await loadInitialRoute(
1083
+ const container = document.getElementById(APP_CONTAINER_ID);
1084
+ if (!container) {
1085
+ console.error(`
1086
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID} not found`);
1087
+ console.error("\u{1F4A1} This usually means:");
1088
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
1089
+ console.error(" \u2022 The container was removed before hydration");
1090
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
1091
+ return;
1092
+ }
1093
+ const initialData = getWindowData();
1094
+ const initialUrl = (initialData?.pathname || window.location.pathname) + window.location.search;
1095
+ if (initialData?.props) {
1096
+ setPreservedLayoutProps(initialData.props);
1097
+ }
1098
+ const routerPathname = initialData?.pathname || window.location.pathname;
1099
+ initializeRouterData(routerPathname + window.location.search, initialData);
1100
+ await hydrateInitialRoute(
1101
+ container,
665
1102
  initialUrl,
666
1103
  initialData,
667
1104
  routes,
668
1105
  notFoundRoute,
669
1106
  errorRoute
670
1107
  );
671
- if (initialData?.metadata) {
672
- applyMetadata(initialData.metadata);
673
- }
674
- hydrateRoot(
675
- container,
676
- /* @__PURE__ */ jsx3(
677
- AppShell,
678
- {
679
- initialState,
680
- routes,
681
- notFoundRoute,
682
- errorRoute
683
- }
684
- )
685
- );
686
1108
  } catch (error) {
687
- console.error(
688
- "[client] Error loading initial route components for",
689
- initialUrl,
690
- error
691
- );
1109
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
1110
+ console.error(error);
1111
+ if (error instanceof Error) {
1112
+ console.error("\nError details:");
1113
+ console.error(` Message: ${error.message}`);
1114
+ if (error.stack) {
1115
+ console.error(` Stack: ${error.stack}`);
1116
+ }
1117
+ }
1118
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
692
1119
  window.location.reload();
693
1120
  }
694
1121
  })();