@lolyjs/core 0.2.0-alpha.2 → 0.2.0-alpha.20

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 (47) hide show
  1. package/LICENCE.md +9 -0
  2. package/README.md +1074 -761
  3. package/dist/{bootstrap-BiCQmSkx.d.mts → bootstrap-BfGTMUkj.d.mts} +19 -0
  4. package/dist/{bootstrap-BiCQmSkx.d.ts → bootstrap-BfGTMUkj.d.ts} +19 -0
  5. package/dist/cli.cjs +16933 -4416
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +16943 -4416
  8. package/dist/cli.js.map +1 -1
  9. package/dist/index.cjs +14387 -1372
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.mts +295 -57
  12. package/dist/index.d.ts +295 -57
  13. package/dist/index.js +15621 -2597
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.types-DMOO-uvF.d.mts +221 -0
  16. package/dist/index.types-DMOO-uvF.d.ts +221 -0
  17. package/dist/react/cache.cjs +107 -32
  18. package/dist/react/cache.cjs.map +1 -1
  19. package/dist/react/cache.d.mts +27 -21
  20. package/dist/react/cache.d.ts +27 -21
  21. package/dist/react/cache.js +107 -32
  22. package/dist/react/cache.js.map +1 -1
  23. package/dist/react/components.cjs +10 -8
  24. package/dist/react/components.cjs.map +1 -1
  25. package/dist/react/components.js +10 -8
  26. package/dist/react/components.js.map +1 -1
  27. package/dist/react/hooks.cjs +208 -26
  28. package/dist/react/hooks.cjs.map +1 -1
  29. package/dist/react/hooks.d.mts +75 -15
  30. package/dist/react/hooks.d.ts +75 -15
  31. package/dist/react/hooks.js +208 -26
  32. package/dist/react/hooks.js.map +1 -1
  33. package/dist/react/sockets.cjs +13 -6
  34. package/dist/react/sockets.cjs.map +1 -1
  35. package/dist/react/sockets.js +13 -6
  36. package/dist/react/sockets.js.map +1 -1
  37. package/dist/react/themes.cjs +61 -18
  38. package/dist/react/themes.cjs.map +1 -1
  39. package/dist/react/themes.js +63 -20
  40. package/dist/react/themes.js.map +1 -1
  41. package/dist/runtime.cjs +544 -111
  42. package/dist/runtime.cjs.map +1 -1
  43. package/dist/runtime.d.mts +2 -2
  44. package/dist/runtime.d.ts +2 -2
  45. package/dist/runtime.js +540 -107
  46. package/dist/runtime.js.map +1 -1
  47. package/package.json +49 -4
package/dist/runtime.js CHANGED
@@ -3,12 +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() {
13
+ if (typeof window === "undefined") {
14
+ return null;
15
+ }
10
16
  return window[WINDOW_DATA_KEY] ?? null;
11
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
+ }
12
40
  function setWindowData(data) {
13
41
  window[WINDOW_DATA_KEY] = data;
14
42
  if (typeof window !== "undefined") {
@@ -19,6 +47,16 @@ function setWindowData(data) {
19
47
  );
20
48
  }
21
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
+ }
22
60
  function getCurrentTheme() {
23
61
  return getWindowData()?.theme ?? null;
24
62
  }
@@ -64,26 +102,162 @@ function matchRouteClient(pathWithSearch, routes) {
64
102
  }
65
103
 
66
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
+ }
67
129
  function applyMetadata(md) {
68
130
  if (!md) return;
69
131
  if (md.title) {
70
132
  document.title = md.title;
71
133
  }
72
134
  if (md.description) {
73
- let meta = document.querySelector(
74
- 'meta[name="description"]'
75
- );
76
- if (!meta) {
77
- meta = document.createElement("meta");
78
- meta.name = "description";
79
- document.head.appendChild(meta);
80
- }
135
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
81
136
  meta.content = md.description;
82
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
+ }
83
257
  }
84
258
 
85
259
  // modules/runtime/client/AppShell.tsx
86
- import { useEffect, useState, useRef } from "react";
260
+ import { useEffect, useState, useRef, useCallback } from "react";
87
261
 
88
262
  // modules/runtime/client/RouterView.tsx
89
263
  import { jsx } from "react/jsx-runtime";
@@ -192,14 +366,16 @@ function deleteCacheEntry(key) {
192
366
  function buildDataUrl(url) {
193
367
  return url + (url.includes("?") ? "&" : "?") + "__fw_data=1";
194
368
  }
195
- async function fetchRouteDataOnce(url) {
369
+ async function fetchRouteDataOnce(url, skipLayoutHooks = true) {
196
370
  const dataUrl = buildDataUrl(url);
197
- const res = await fetch(dataUrl, {
198
- headers: {
199
- "x-fw-data": "1",
200
- Accept: "application/json"
201
- }
202
- });
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 });
203
379
  let json = {};
204
380
  try {
205
381
  const text = await res.text();
@@ -225,7 +401,7 @@ async function getRouteData(url, options) {
225
401
  deleteCacheEntry(key);
226
402
  }
227
403
  const entry = dataCache.get(key);
228
- if (entry) {
404
+ if (entry && !options?.revalidate) {
229
405
  if (entry.status === "fulfilled") {
230
406
  updateLRU(key);
231
407
  return entry.value;
@@ -234,12 +410,29 @@ async function getRouteData(url, options) {
234
410
  return entry.promise;
235
411
  }
236
412
  }
237
- const promise = fetchRouteDataOnce(url).then((value) => {
238
- 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
+ }
239
429
  return value;
240
430
  }).catch((error) => {
241
431
  console.error("[client][cache] Error fetching route data:", error);
242
- 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
+ }
243
436
  throw error;
244
437
  });
245
438
  dataCache.set(key, { status: "pending", promise });
@@ -264,10 +457,22 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
264
457
  } else if (json.theme) {
265
458
  theme = json.theme;
266
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
+ };
267
473
  const errorProps = {
268
- ...json.props || {
269
- error: json.message || "An error occurred"
270
- },
474
+ ...layoutProps,
475
+ ...pageProps,
271
476
  theme
272
477
  };
273
478
  const windowData = {
@@ -280,6 +485,13 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
280
485
  error: true
281
486
  };
282
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);
283
495
  setState({
284
496
  url: nextUrl,
285
497
  route: errorRoute,
@@ -289,10 +501,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
289
501
  });
290
502
  return true;
291
503
  } catch (loadError) {
292
- console.error(
293
- "[client] Error loading error route components:",
294
- loadError
295
- );
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");
296
513
  window.location.href = nextUrl;
297
514
  return false;
298
515
  }
@@ -312,8 +529,20 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
312
529
  } else if (json.theme) {
313
530
  theme = json.theme;
314
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 ?? {};
315
543
  const notFoundProps = {
316
- ...json.props ?? {},
544
+ ...layoutProps,
545
+ ...pageProps,
317
546
  theme
318
547
  };
319
548
  const windowData = {
@@ -326,6 +555,13 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
326
555
  error: false
327
556
  };
328
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);
329
565
  if (notFoundRoute) {
330
566
  const components = await notFoundRoute.load();
331
567
  setState({
@@ -363,8 +599,20 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
363
599
  } else if (json.theme) {
364
600
  theme = json.theme;
365
601
  }
366
- const newProps = {
367
- ...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,
368
616
  theme
369
617
  // Always include theme
370
618
  };
@@ -376,14 +624,25 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
376
624
  const windowData = {
377
625
  pathname: nextUrl,
378
626
  params: matched.params,
379
- props: newProps,
627
+ props: combinedProps,
380
628
  metadata: json.metadata ?? null,
381
629
  theme,
382
630
  notFound: false,
383
631
  error: false
384
632
  };
385
633
  setWindowData(windowData);
386
- const components = await matched.route.load();
634
+ const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
635
+ const routerData = {
636
+ pathname: url.pathname,
637
+ params: matched.params,
638
+ searchParams: Object.fromEntries(url.searchParams.entries())
639
+ };
640
+ setRouterData(routerData);
641
+ const prefetched = prefetchedRoutes.get(matched.route);
642
+ const components = prefetched ? await prefetched : await matched.route.load();
643
+ if (!prefetched) {
644
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
645
+ }
387
646
  window.scrollTo({
388
647
  top: 0,
389
648
  behavior: "smooth"
@@ -393,7 +652,7 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
393
652
  route: matched.route,
394
653
  params: matched.params,
395
654
  components,
396
- props: newProps
655
+ props: combinedProps
397
656
  });
398
657
  return true;
399
658
  }
@@ -422,7 +681,7 @@ async function navigate(nextUrl, handlers, options) {
422
681
  }
423
682
  }
424
683
  if (!ok) {
425
- if (json && json.redirect) {
684
+ if (json?.redirect) {
426
685
  window.location.href = json.redirect.destination;
427
686
  return;
428
687
  }
@@ -443,6 +702,47 @@ async function navigate(nextUrl, handlers, options) {
443
702
  window.location.href = nextUrl;
444
703
  }
445
704
  }
705
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
706
+ function prefetchRoute(url, routes, notFoundRoute) {
707
+ const [pathname] = url.split("?");
708
+ const matched = matchRouteClient(pathname, routes);
709
+ if (!matched) {
710
+ if (notFoundRoute) {
711
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
712
+ if (!existing2) {
713
+ const promise = notFoundRoute.load();
714
+ prefetchedRoutes.set(notFoundRoute, promise);
715
+ }
716
+ }
717
+ return;
718
+ }
719
+ const existing = prefetchedRoutes.get(matched.route);
720
+ if (!existing) {
721
+ const promise = matched.route.load();
722
+ prefetchedRoutes.set(matched.route, promise);
723
+ }
724
+ }
725
+ function createHoverHandler(routes, notFoundRoute) {
726
+ return function handleHover(ev) {
727
+ try {
728
+ const target = ev.target;
729
+ if (!target) return;
730
+ const anchor = target.closest("a[href]");
731
+ if (!anchor) return;
732
+ const href = anchor.getAttribute("href");
733
+ if (!href) return;
734
+ if (href.startsWith("#")) return;
735
+ const url = new URL(href, window.location.href);
736
+ if (url.origin !== window.location.origin) return;
737
+ if (anchor.target && anchor.target !== "_self") return;
738
+ const nextUrl = url.pathname + url.search;
739
+ const currentUrl = window.location.pathname + window.location.search;
740
+ if (nextUrl === currentUrl) return;
741
+ prefetchRoute(nextUrl, routes, notFoundRoute);
742
+ } catch (error) {
743
+ }
744
+ };
745
+ }
446
746
  function createClickHandler(navigate2) {
447
747
  return function handleClick(ev) {
448
748
  try {
@@ -502,6 +802,10 @@ function createPopStateHandler(navigate2) {
502
802
  };
503
803
  }
504
804
 
805
+ // modules/runtime/client/RouterContext.tsx
806
+ import { createContext, useContext } from "react";
807
+ var RouterContext = createContext(null);
808
+
505
809
  // modules/runtime/client/AppShell.tsx
506
810
  import { jsx as jsx2 } from "react/jsx-runtime";
507
811
  function AppShell({
@@ -525,27 +829,153 @@ function AppShell({
525
829
  errorRoute
526
830
  };
527
831
  }, [routes, notFoundRoute, errorRoute]);
832
+ const handleNavigate = useCallback(
833
+ async (nextUrl, options) => {
834
+ await navigate(nextUrl, handlersRef.current, {
835
+ revalidate: options?.revalidate
836
+ });
837
+ },
838
+ []
839
+ );
840
+ useEffect(() => {
841
+ if (typeof window !== "undefined") {
842
+ window[ROUTER_NAVIGATE_KEY] = handleNavigate;
843
+ return () => {
844
+ delete window[ROUTER_NAVIGATE_KEY];
845
+ };
846
+ }
847
+ }, [handleNavigate]);
528
848
  useEffect(() => {
529
849
  let isMounted = true;
530
- async function handleNavigate(nextUrl, options) {
850
+ async function handleNavigateInternal(nextUrl, options) {
531
851
  if (!isMounted) return;
532
852
  await navigate(nextUrl, handlersRef.current, options);
533
853
  }
534
- const handleClick = createClickHandler(handleNavigate);
535
- const handlePopState = createPopStateHandler(handleNavigate);
854
+ const handleClick = createClickHandler(handleNavigateInternal);
855
+ const handlePopState = createPopStateHandler(handleNavigateInternal);
856
+ const handleHover = createHoverHandler(routes, notFoundRoute);
536
857
  window.addEventListener("click", handleClick, false);
537
858
  window.addEventListener("popstate", handlePopState, false);
859
+ window.addEventListener("mouseover", handleHover, false);
538
860
  return () => {
539
861
  isMounted = false;
540
862
  window.removeEventListener("click", handleClick, false);
541
863
  window.removeEventListener("popstate", handlePopState, false);
864
+ window.removeEventListener("mouseover", handleHover, false);
865
+ };
866
+ }, [routes, notFoundRoute]);
867
+ useEffect(() => {
868
+ const handleDataRefresh = () => {
869
+ const freshData = window[WINDOW_DATA_KEY];
870
+ if (!freshData) return;
871
+ const currentPathname = window.location.pathname;
872
+ const freshPathname = freshData.pathname;
873
+ if (freshPathname === currentPathname) {
874
+ if (freshData.metadata !== void 0) {
875
+ applyMetadata(freshData.metadata);
876
+ }
877
+ setState((prevState) => ({
878
+ ...prevState,
879
+ props: freshData.props ?? prevState.props,
880
+ params: freshData.params ?? prevState.params
881
+ }));
882
+ }
883
+ };
884
+ window.addEventListener("fw-data-refresh", handleDataRefresh);
885
+ return () => {
886
+ window.removeEventListener("fw-data-refresh", handleDataRefresh);
542
887
  };
543
888
  }, []);
544
889
  const isError = state.route === errorRoute;
545
890
  const isNotFound = state.route === notFoundRoute;
546
891
  const routeType = isError ? "error" : isNotFound ? "notfound" : "normal";
547
892
  const routeKey = `${state.url}:${routeType}`;
548
- return /* @__PURE__ */ jsx2(RouterView, { state }, routeKey);
893
+ return /* @__PURE__ */ jsx2(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ jsx2(RouterView, { state }, routeKey) });
894
+ }
895
+
896
+ // modules/runtime/client/hot-reload.ts
897
+ function setupHotReload() {
898
+ const nodeEnv = process.env.NODE_ENV || "production";
899
+ const isDev = nodeEnv === "development";
900
+ console.log(`[hot-reload] NODE_ENV: ${nodeEnv}, isDev: ${isDev}`);
901
+ if (!isDev) {
902
+ console.log("[hot-reload] Skipping hot reload setup (not in development mode)");
903
+ return;
904
+ }
905
+ console.log("[hot-reload] Setting up hot reload client...");
906
+ let eventSource = null;
907
+ let reloadTimeout = null;
908
+ let reconnectTimeout = null;
909
+ let reconnectAttempts = 0;
910
+ const MAX_RECONNECT_ATTEMPTS = 10;
911
+ const RECONNECT_DELAY = 1e3;
912
+ const RELOAD_DELAY = 100;
913
+ function connect() {
914
+ try {
915
+ if (eventSource) {
916
+ console.log("[hot-reload] Closing existing EventSource connection");
917
+ eventSource.close();
918
+ }
919
+ const endpoint = "/__fw/hot";
920
+ eventSource = new EventSource(endpoint);
921
+ eventSource.addEventListener("ping", (event) => {
922
+ if ("data" in event) {
923
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
924
+ }
925
+ reconnectAttempts = 0;
926
+ });
927
+ eventSource.addEventListener("message", (event) => {
928
+ const data = event.data;
929
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
930
+ const filePath = data.slice(7);
931
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
932
+ if (reloadTimeout) {
933
+ clearTimeout(reloadTimeout);
934
+ }
935
+ reloadTimeout = setTimeout(() => {
936
+ try {
937
+ window.location.reload();
938
+ } catch (error) {
939
+ console.error("[hot-reload] \u274C Error reloading page:", error);
940
+ setTimeout(() => window.location.reload(), 100);
941
+ }
942
+ }, RELOAD_DELAY);
943
+ }
944
+ });
945
+ eventSource.onopen = () => {
946
+ reconnectAttempts = 0;
947
+ };
948
+ eventSource.onerror = (error) => {
949
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
950
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
951
+ if (eventSource?.readyState === EventSource.CONNECTING) {
952
+ console.log("[hot-reload] \u23F3 Still connecting...");
953
+ return;
954
+ } else if (eventSource?.readyState === EventSource.OPEN) {
955
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
956
+ } else {
957
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
958
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
959
+ reconnectAttempts++;
960
+ const delay = RECONNECT_DELAY * reconnectAttempts;
961
+ if (reconnectTimeout) {
962
+ clearTimeout(reconnectTimeout);
963
+ }
964
+ reconnectTimeout = setTimeout(() => {
965
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
966
+ connect();
967
+ }, delay);
968
+ } else {
969
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
970
+ }
971
+ }
972
+ };
973
+ } catch (error) {
974
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
975
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
976
+ }
977
+ }
978
+ connect();
549
979
  }
550
980
 
551
981
  // modules/runtime/client/bootstrap.tsx
@@ -589,91 +1019,94 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
589
1019
  props: initialData?.props ?? {}
590
1020
  };
591
1021
  }
592
- function setupHotReload() {
593
- const nodeEnv = typeof process !== "undefined" && process?.env?.NODE_ENV || "production";
594
- const isDev = nodeEnv !== "production";
595
- if (!isDev) {
596
- return;
1022
+ function initializeRouterData(initialUrl, initialData) {
1023
+ let routerData = getRouterData();
1024
+ if (!routerData) {
1025
+ const url = new URL(initialUrl, window.location.origin);
1026
+ routerData = {
1027
+ pathname: url.pathname,
1028
+ params: initialData?.params || {},
1029
+ searchParams: Object.fromEntries(url.searchParams.entries())
1030
+ };
1031
+ setRouterData(routerData);
597
1032
  }
1033
+ }
1034
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
598
1035
  try {
599
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
600
- const eventSource = new EventSource("/__fw/hot");
601
- let reloadTimeout = null;
602
- eventSource.addEventListener("message", (event) => {
603
- const data = event.data;
604
- if (data && data.startsWith("reload:")) {
605
- const filePath = data.slice(7);
606
- console.log(`[hot-reload] File changed: ${filePath}`);
607
- if (reloadTimeout) {
608
- clearTimeout(reloadTimeout);
609
- }
610
- reloadTimeout = setTimeout(() => {
611
- console.log("[hot-reload] Reloading page...");
612
- window.location.reload();
613
- }, 500);
614
- }
615
- });
616
- eventSource.addEventListener("ping", () => {
617
- console.log("[hot-reload] \u2713 Connected to hot reload server");
618
- });
619
- eventSource.onopen = () => {
620
- console.log("[hot-reload] \u2713 SSE connection opened");
621
- };
622
- eventSource.onerror = (error) => {
623
- const states = ["CONNECTING", "OPEN", "CLOSED"];
624
- const state = states[eventSource.readyState] || "UNKNOWN";
625
- if (eventSource.readyState === EventSource.CONNECTING) {
626
- console.log("[hot-reload] Connecting...");
627
- } else if (eventSource.readyState === EventSource.OPEN) {
628
- console.warn("[hot-reload] Connection error (but connection is open):", error);
629
- } else {
630
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
1036
+ const initialState = await loadInitialRoute(
1037
+ initialUrl,
1038
+ initialData,
1039
+ routes,
1040
+ notFoundRoute,
1041
+ errorRoute
1042
+ );
1043
+ if (initialData?.metadata) {
1044
+ try {
1045
+ applyMetadata(initialData.metadata);
1046
+ } catch (metadataError) {
1047
+ console.warn("[client] Error applying metadata:", metadataError);
631
1048
  }
632
- };
1049
+ }
1050
+ hydrateRoot(
1051
+ container,
1052
+ /* @__PURE__ */ jsx3(
1053
+ AppShell,
1054
+ {
1055
+ initialState,
1056
+ routes,
1057
+ notFoundRoute,
1058
+ errorRoute
1059
+ }
1060
+ )
1061
+ );
633
1062
  } catch (error) {
634
- console.log("[hot-reload] EventSource not supported or error:", error);
1063
+ console.error(
1064
+ "[client] Error loading initial route components for",
1065
+ initialUrl,
1066
+ error
1067
+ );
1068
+ throw error;
635
1069
  }
636
1070
  }
637
1071
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
638
- console.log("[client] Bootstrap starting, setting up hot reload...");
639
1072
  setupHotReload();
640
- (async function bootstrap() {
641
- const container = document.getElementById(APP_CONTAINER_ID);
642
- const initialData = getWindowData();
643
- if (!container) {
644
- console.error(`Container #${APP_CONTAINER_ID} not found for hydration`);
645
- return;
646
- }
647
- const initialUrl = window.location.pathname + window.location.search;
1073
+ (async () => {
648
1074
  try {
649
- const initialState = await loadInitialRoute(
1075
+ const container = document.getElementById(APP_CONTAINER_ID);
1076
+ if (!container) {
1077
+ console.error(`
1078
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID} not found`);
1079
+ console.error("\u{1F4A1} This usually means:");
1080
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
1081
+ console.error(" \u2022 The container was removed before hydration");
1082
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
1083
+ return;
1084
+ }
1085
+ const initialData = getWindowData();
1086
+ const initialUrl = window.location.pathname + window.location.search;
1087
+ if (initialData?.props) {
1088
+ setPreservedLayoutProps(initialData.props);
1089
+ }
1090
+ initializeRouterData(initialUrl, initialData);
1091
+ await hydrateInitialRoute(
1092
+ container,
650
1093
  initialUrl,
651
1094
  initialData,
652
1095
  routes,
653
1096
  notFoundRoute,
654
1097
  errorRoute
655
1098
  );
656
- if (initialData?.metadata) {
657
- applyMetadata(initialData.metadata);
658
- }
659
- hydrateRoot(
660
- container,
661
- /* @__PURE__ */ jsx3(
662
- AppShell,
663
- {
664
- initialState,
665
- routes,
666
- notFoundRoute,
667
- errorRoute
668
- }
669
- )
670
- );
671
1099
  } catch (error) {
672
- console.error(
673
- "[client] Error loading initial route components for",
674
- initialUrl,
675
- error
676
- );
1100
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
1101
+ console.error(error);
1102
+ if (error instanceof Error) {
1103
+ console.error("\nError details:");
1104
+ console.error(` Message: ${error.message}`);
1105
+ if (error.stack) {
1106
+ console.error(` Stack: ${error.stack}`);
1107
+ }
1108
+ }
1109
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
677
1110
  window.location.reload();
678
1111
  }
679
1112
  })();