@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.cjs CHANGED
@@ -29,12 +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() {
39
+ if (typeof window === "undefined") {
40
+ return null;
41
+ }
36
42
  return window[WINDOW_DATA_KEY] ?? null;
37
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
+ }
38
66
  function setWindowData(data) {
39
67
  window[WINDOW_DATA_KEY] = data;
40
68
  if (typeof window !== "undefined") {
@@ -45,6 +73,16 @@ function setWindowData(data) {
45
73
  );
46
74
  }
47
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
+ }
48
86
  function getCurrentTheme() {
49
87
  return getWindowData()?.theme ?? null;
50
88
  }
@@ -90,26 +128,162 @@ function matchRouteClient(pathWithSearch, routes) {
90
128
  }
91
129
 
92
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
+ }
93
155
  function applyMetadata(md) {
94
156
  if (!md) return;
95
157
  if (md.title) {
96
158
  document.title = md.title;
97
159
  }
98
160
  if (md.description) {
99
- let meta = document.querySelector(
100
- 'meta[name="description"]'
101
- );
102
- if (!meta) {
103
- meta = document.createElement("meta");
104
- meta.name = "description";
105
- document.head.appendChild(meta);
106
- }
161
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
107
162
  meta.content = md.description;
108
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
+ }
109
283
  }
110
284
 
111
285
  // modules/runtime/client/AppShell.tsx
112
- var import_react = require("react");
286
+ var import_react2 = require("react");
113
287
 
114
288
  // modules/runtime/client/RouterView.tsx
115
289
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -218,14 +392,16 @@ function deleteCacheEntry(key) {
218
392
  function buildDataUrl(url) {
219
393
  return url + (url.includes("?") ? "&" : "?") + "__fw_data=1";
220
394
  }
221
- async function fetchRouteDataOnce(url) {
395
+ async function fetchRouteDataOnce(url, skipLayoutHooks = true) {
222
396
  const dataUrl = buildDataUrl(url);
223
- const res = await fetch(dataUrl, {
224
- headers: {
225
- "x-fw-data": "1",
226
- Accept: "application/json"
227
- }
228
- });
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 });
229
405
  let json = {};
230
406
  try {
231
407
  const text = await res.text();
@@ -251,7 +427,7 @@ async function getRouteData(url, options) {
251
427
  deleteCacheEntry(key);
252
428
  }
253
429
  const entry = dataCache.get(key);
254
- if (entry) {
430
+ if (entry && !options?.revalidate) {
255
431
  if (entry.status === "fulfilled") {
256
432
  updateLRU(key);
257
433
  return entry.value;
@@ -260,12 +436,29 @@ async function getRouteData(url, options) {
260
436
  return entry.promise;
261
437
  }
262
438
  }
263
- const promise = fetchRouteDataOnce(url).then((value) => {
264
- 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
+ }
265
455
  return value;
266
456
  }).catch((error) => {
267
457
  console.error("[client][cache] Error fetching route data:", error);
268
- 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
+ }
269
462
  throw error;
270
463
  });
271
464
  dataCache.set(key, { status: "pending", promise });
@@ -290,10 +483,22 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
290
483
  } else if (json.theme) {
291
484
  theme = json.theme;
292
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
+ };
293
499
  const errorProps = {
294
- ...json.props || {
295
- error: json.message || "An error occurred"
296
- },
500
+ ...layoutProps,
501
+ ...pageProps,
297
502
  theme
298
503
  };
299
504
  const windowData = {
@@ -306,6 +511,13 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
306
511
  error: true
307
512
  };
308
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);
309
521
  setState({
310
522
  url: nextUrl,
311
523
  route: errorRoute,
@@ -315,10 +527,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
315
527
  });
316
528
  return true;
317
529
  } catch (loadError) {
318
- console.error(
319
- "[client] Error loading error route components:",
320
- loadError
321
- );
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");
322
539
  window.location.href = nextUrl;
323
540
  return false;
324
541
  }
@@ -338,8 +555,20 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
338
555
  } else if (json.theme) {
339
556
  theme = json.theme;
340
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 ?? {};
341
569
  const notFoundProps = {
342
- ...json.props ?? {},
570
+ ...layoutProps,
571
+ ...pageProps,
343
572
  theme
344
573
  };
345
574
  const windowData = {
@@ -352,6 +581,13 @@ async function handleNotFoundRoute(nextUrl, json, notFoundRoute, setState) {
352
581
  error: false
353
582
  };
354
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);
355
591
  if (notFoundRoute) {
356
592
  const components = await notFoundRoute.load();
357
593
  setState({
@@ -389,8 +625,20 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
389
625
  } else if (json.theme) {
390
626
  theme = json.theme;
391
627
  }
392
- const newProps = {
393
- ...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,
394
642
  theme
395
643
  // Always include theme
396
644
  };
@@ -402,14 +650,25 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
402
650
  const windowData = {
403
651
  pathname: nextUrl,
404
652
  params: matched.params,
405
- props: newProps,
653
+ props: combinedProps,
406
654
  metadata: json.metadata ?? null,
407
655
  theme,
408
656
  notFound: false,
409
657
  error: false
410
658
  };
411
659
  setWindowData(windowData);
412
- const components = await matched.route.load();
660
+ const url = new URL(nextUrl, typeof window !== "undefined" ? window.location.origin : "http://localhost");
661
+ const routerData = {
662
+ pathname: url.pathname,
663
+ params: matched.params,
664
+ searchParams: Object.fromEntries(url.searchParams.entries())
665
+ };
666
+ setRouterData(routerData);
667
+ const prefetched = prefetchedRoutes.get(matched.route);
668
+ const components = prefetched ? await prefetched : await matched.route.load();
669
+ if (!prefetched) {
670
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
671
+ }
413
672
  window.scrollTo({
414
673
  top: 0,
415
674
  behavior: "smooth"
@@ -419,7 +678,7 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
419
678
  route: matched.route,
420
679
  params: matched.params,
421
680
  components,
422
- props: newProps
681
+ props: combinedProps
423
682
  });
424
683
  return true;
425
684
  }
@@ -448,7 +707,7 @@ async function navigate(nextUrl, handlers, options) {
448
707
  }
449
708
  }
450
709
  if (!ok) {
451
- if (json && json.redirect) {
710
+ if (json?.redirect) {
452
711
  window.location.href = json.redirect.destination;
453
712
  return;
454
713
  }
@@ -469,6 +728,47 @@ async function navigate(nextUrl, handlers, options) {
469
728
  window.location.href = nextUrl;
470
729
  }
471
730
  }
731
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
732
+ function prefetchRoute(url, routes, notFoundRoute) {
733
+ const [pathname] = url.split("?");
734
+ const matched = matchRouteClient(pathname, routes);
735
+ if (!matched) {
736
+ if (notFoundRoute) {
737
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
738
+ if (!existing2) {
739
+ const promise = notFoundRoute.load();
740
+ prefetchedRoutes.set(notFoundRoute, promise);
741
+ }
742
+ }
743
+ return;
744
+ }
745
+ const existing = prefetchedRoutes.get(matched.route);
746
+ if (!existing) {
747
+ const promise = matched.route.load();
748
+ prefetchedRoutes.set(matched.route, promise);
749
+ }
750
+ }
751
+ function createHoverHandler(routes, notFoundRoute) {
752
+ return function handleHover(ev) {
753
+ try {
754
+ const target = ev.target;
755
+ if (!target) return;
756
+ const anchor = target.closest("a[href]");
757
+ if (!anchor) return;
758
+ const href = anchor.getAttribute("href");
759
+ if (!href) return;
760
+ if (href.startsWith("#")) return;
761
+ const url = new URL(href, window.location.href);
762
+ if (url.origin !== window.location.origin) return;
763
+ if (anchor.target && anchor.target !== "_self") return;
764
+ const nextUrl = url.pathname + url.search;
765
+ const currentUrl = window.location.pathname + window.location.search;
766
+ if (nextUrl === currentUrl) return;
767
+ prefetchRoute(nextUrl, routes, notFoundRoute);
768
+ } catch (error) {
769
+ }
770
+ };
771
+ }
472
772
  function createClickHandler(navigate2) {
473
773
  return function handleClick(ev) {
474
774
  try {
@@ -528,6 +828,10 @@ function createPopStateHandler(navigate2) {
528
828
  };
529
829
  }
530
830
 
831
+ // modules/runtime/client/RouterContext.tsx
832
+ var import_react = require("react");
833
+ var RouterContext = (0, import_react.createContext)(null);
834
+
531
835
  // modules/runtime/client/AppShell.tsx
532
836
  var import_jsx_runtime2 = require("react/jsx-runtime");
533
837
  function AppShell({
@@ -536,14 +840,14 @@ function AppShell({
536
840
  notFoundRoute,
537
841
  errorRoute
538
842
  }) {
539
- const [state, setState] = (0, import_react.useState)(initialState);
540
- const handlersRef = (0, import_react.useRef)({
843
+ const [state, setState] = (0, import_react2.useState)(initialState);
844
+ const handlersRef = (0, import_react2.useRef)({
541
845
  setState,
542
846
  routes,
543
847
  notFoundRoute,
544
848
  errorRoute
545
849
  });
546
- (0, import_react.useEffect)(() => {
850
+ (0, import_react2.useEffect)(() => {
547
851
  handlersRef.current = {
548
852
  setState,
549
853
  routes,
@@ -551,27 +855,153 @@ function AppShell({
551
855
  errorRoute
552
856
  };
553
857
  }, [routes, notFoundRoute, errorRoute]);
554
- (0, import_react.useEffect)(() => {
858
+ const handleNavigate = (0, import_react2.useCallback)(
859
+ async (nextUrl, options) => {
860
+ await navigate(nextUrl, handlersRef.current, {
861
+ revalidate: options?.revalidate
862
+ });
863
+ },
864
+ []
865
+ );
866
+ (0, import_react2.useEffect)(() => {
867
+ if (typeof window !== "undefined") {
868
+ window[ROUTER_NAVIGATE_KEY] = handleNavigate;
869
+ return () => {
870
+ delete window[ROUTER_NAVIGATE_KEY];
871
+ };
872
+ }
873
+ }, [handleNavigate]);
874
+ (0, import_react2.useEffect)(() => {
555
875
  let isMounted = true;
556
- async function handleNavigate(nextUrl, options) {
876
+ async function handleNavigateInternal(nextUrl, options) {
557
877
  if (!isMounted) return;
558
878
  await navigate(nextUrl, handlersRef.current, options);
559
879
  }
560
- const handleClick = createClickHandler(handleNavigate);
561
- const handlePopState = createPopStateHandler(handleNavigate);
880
+ const handleClick = createClickHandler(handleNavigateInternal);
881
+ const handlePopState = createPopStateHandler(handleNavigateInternal);
882
+ const handleHover = createHoverHandler(routes, notFoundRoute);
562
883
  window.addEventListener("click", handleClick, false);
563
884
  window.addEventListener("popstate", handlePopState, false);
885
+ window.addEventListener("mouseover", handleHover, false);
564
886
  return () => {
565
887
  isMounted = false;
566
888
  window.removeEventListener("click", handleClick, false);
567
889
  window.removeEventListener("popstate", handlePopState, false);
890
+ window.removeEventListener("mouseover", handleHover, false);
891
+ };
892
+ }, [routes, notFoundRoute]);
893
+ (0, import_react2.useEffect)(() => {
894
+ const handleDataRefresh = () => {
895
+ const freshData = window[WINDOW_DATA_KEY];
896
+ if (!freshData) return;
897
+ const currentPathname = window.location.pathname;
898
+ const freshPathname = freshData.pathname;
899
+ if (freshPathname === currentPathname) {
900
+ if (freshData.metadata !== void 0) {
901
+ applyMetadata(freshData.metadata);
902
+ }
903
+ setState((prevState) => ({
904
+ ...prevState,
905
+ props: freshData.props ?? prevState.props,
906
+ params: freshData.params ?? prevState.params
907
+ }));
908
+ }
909
+ };
910
+ window.addEventListener("fw-data-refresh", handleDataRefresh);
911
+ return () => {
912
+ window.removeEventListener("fw-data-refresh", handleDataRefresh);
568
913
  };
569
914
  }, []);
570
915
  const isError = state.route === errorRoute;
571
916
  const isNotFound = state.route === notFoundRoute;
572
917
  const routeType = isError ? "error" : isNotFound ? "notfound" : "normal";
573
918
  const routeKey = `${state.url}:${routeType}`;
574
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterView, { state }, routeKey);
919
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterView, { state }, routeKey) });
920
+ }
921
+
922
+ // modules/runtime/client/hot-reload.ts
923
+ function setupHotReload() {
924
+ const nodeEnv = process.env.NODE_ENV || "production";
925
+ const isDev = nodeEnv === "development";
926
+ console.log(`[hot-reload] NODE_ENV: ${nodeEnv}, isDev: ${isDev}`);
927
+ if (!isDev) {
928
+ console.log("[hot-reload] Skipping hot reload setup (not in development mode)");
929
+ return;
930
+ }
931
+ console.log("[hot-reload] Setting up hot reload client...");
932
+ let eventSource = null;
933
+ let reloadTimeout = null;
934
+ let reconnectTimeout = null;
935
+ let reconnectAttempts = 0;
936
+ const MAX_RECONNECT_ATTEMPTS = 10;
937
+ const RECONNECT_DELAY = 1e3;
938
+ const RELOAD_DELAY = 100;
939
+ function connect() {
940
+ try {
941
+ if (eventSource) {
942
+ console.log("[hot-reload] Closing existing EventSource connection");
943
+ eventSource.close();
944
+ }
945
+ const endpoint = "/__fw/hot";
946
+ eventSource = new EventSource(endpoint);
947
+ eventSource.addEventListener("ping", (event) => {
948
+ if ("data" in event) {
949
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
950
+ }
951
+ reconnectAttempts = 0;
952
+ });
953
+ eventSource.addEventListener("message", (event) => {
954
+ const data = event.data;
955
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
956
+ const filePath = data.slice(7);
957
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
958
+ if (reloadTimeout) {
959
+ clearTimeout(reloadTimeout);
960
+ }
961
+ reloadTimeout = setTimeout(() => {
962
+ try {
963
+ window.location.reload();
964
+ } catch (error) {
965
+ console.error("[hot-reload] \u274C Error reloading page:", error);
966
+ setTimeout(() => window.location.reload(), 100);
967
+ }
968
+ }, RELOAD_DELAY);
969
+ }
970
+ });
971
+ eventSource.onopen = () => {
972
+ reconnectAttempts = 0;
973
+ };
974
+ eventSource.onerror = (error) => {
975
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
976
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
977
+ if (eventSource?.readyState === EventSource.CONNECTING) {
978
+ console.log("[hot-reload] \u23F3 Still connecting...");
979
+ return;
980
+ } else if (eventSource?.readyState === EventSource.OPEN) {
981
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
982
+ } else {
983
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
984
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
985
+ reconnectAttempts++;
986
+ const delay = RECONNECT_DELAY * reconnectAttempts;
987
+ if (reconnectTimeout) {
988
+ clearTimeout(reconnectTimeout);
989
+ }
990
+ reconnectTimeout = setTimeout(() => {
991
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
992
+ connect();
993
+ }, delay);
994
+ } else {
995
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
996
+ }
997
+ }
998
+ };
999
+ } catch (error) {
1000
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
1001
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
1002
+ }
1003
+ }
1004
+ connect();
575
1005
  }
576
1006
 
577
1007
  // modules/runtime/client/bootstrap.tsx
@@ -615,91 +1045,94 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
615
1045
  props: initialData?.props ?? {}
616
1046
  };
617
1047
  }
618
- function setupHotReload() {
619
- const nodeEnv = typeof process !== "undefined" && process?.env?.NODE_ENV || "production";
620
- const isDev = nodeEnv !== "production";
621
- if (!isDev) {
622
- return;
1048
+ function initializeRouterData(initialUrl, initialData) {
1049
+ let routerData = getRouterData();
1050
+ if (!routerData) {
1051
+ const url = new URL(initialUrl, window.location.origin);
1052
+ routerData = {
1053
+ pathname: url.pathname,
1054
+ params: initialData?.params || {},
1055
+ searchParams: Object.fromEntries(url.searchParams.entries())
1056
+ };
1057
+ setRouterData(routerData);
623
1058
  }
1059
+ }
1060
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
624
1061
  try {
625
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
626
- const eventSource = new EventSource("/__fw/hot");
627
- let reloadTimeout = null;
628
- eventSource.addEventListener("message", (event) => {
629
- const data = event.data;
630
- if (data && data.startsWith("reload:")) {
631
- const filePath = data.slice(7);
632
- console.log(`[hot-reload] File changed: ${filePath}`);
633
- if (reloadTimeout) {
634
- clearTimeout(reloadTimeout);
635
- }
636
- reloadTimeout = setTimeout(() => {
637
- console.log("[hot-reload] Reloading page...");
638
- window.location.reload();
639
- }, 500);
640
- }
641
- });
642
- eventSource.addEventListener("ping", () => {
643
- console.log("[hot-reload] \u2713 Connected to hot reload server");
644
- });
645
- eventSource.onopen = () => {
646
- console.log("[hot-reload] \u2713 SSE connection opened");
647
- };
648
- eventSource.onerror = (error) => {
649
- const states = ["CONNECTING", "OPEN", "CLOSED"];
650
- const state = states[eventSource.readyState] || "UNKNOWN";
651
- if (eventSource.readyState === EventSource.CONNECTING) {
652
- console.log("[hot-reload] Connecting...");
653
- } else if (eventSource.readyState === EventSource.OPEN) {
654
- console.warn("[hot-reload] Connection error (but connection is open):", error);
655
- } else {
656
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
1062
+ const initialState = await loadInitialRoute(
1063
+ initialUrl,
1064
+ initialData,
1065
+ routes,
1066
+ notFoundRoute,
1067
+ errorRoute
1068
+ );
1069
+ if (initialData?.metadata) {
1070
+ try {
1071
+ applyMetadata(initialData.metadata);
1072
+ } catch (metadataError) {
1073
+ console.warn("[client] Error applying metadata:", metadataError);
657
1074
  }
658
- };
1075
+ }
1076
+ (0, import_client.hydrateRoot)(
1077
+ container,
1078
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1079
+ AppShell,
1080
+ {
1081
+ initialState,
1082
+ routes,
1083
+ notFoundRoute,
1084
+ errorRoute
1085
+ }
1086
+ )
1087
+ );
659
1088
  } catch (error) {
660
- console.log("[hot-reload] EventSource not supported or error:", error);
1089
+ console.error(
1090
+ "[client] Error loading initial route components for",
1091
+ initialUrl,
1092
+ error
1093
+ );
1094
+ throw error;
661
1095
  }
662
1096
  }
663
1097
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
664
- console.log("[client] Bootstrap starting, setting up hot reload...");
665
1098
  setupHotReload();
666
- (async function bootstrap() {
667
- const container = document.getElementById(APP_CONTAINER_ID);
668
- const initialData = getWindowData();
669
- if (!container) {
670
- console.error(`Container #${APP_CONTAINER_ID} not found for hydration`);
671
- return;
672
- }
673
- const initialUrl = window.location.pathname + window.location.search;
1099
+ (async () => {
674
1100
  try {
675
- const initialState = await loadInitialRoute(
1101
+ const container = document.getElementById(APP_CONTAINER_ID);
1102
+ if (!container) {
1103
+ console.error(`
1104
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID} not found`);
1105
+ console.error("\u{1F4A1} This usually means:");
1106
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
1107
+ console.error(" \u2022 The container was removed before hydration");
1108
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
1109
+ return;
1110
+ }
1111
+ const initialData = getWindowData();
1112
+ const initialUrl = window.location.pathname + window.location.search;
1113
+ if (initialData?.props) {
1114
+ setPreservedLayoutProps(initialData.props);
1115
+ }
1116
+ initializeRouterData(initialUrl, initialData);
1117
+ await hydrateInitialRoute(
1118
+ container,
676
1119
  initialUrl,
677
1120
  initialData,
678
1121
  routes,
679
1122
  notFoundRoute,
680
1123
  errorRoute
681
1124
  );
682
- if (initialData?.metadata) {
683
- applyMetadata(initialData.metadata);
684
- }
685
- (0, import_client.hydrateRoot)(
686
- container,
687
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
688
- AppShell,
689
- {
690
- initialState,
691
- routes,
692
- notFoundRoute,
693
- errorRoute
694
- }
695
- )
696
- );
697
1125
  } catch (error) {
698
- console.error(
699
- "[client] Error loading initial route components for",
700
- initialUrl,
701
- error
702
- );
1126
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
1127
+ console.error(error);
1128
+ if (error instanceof Error) {
1129
+ console.error("\nError details:");
1130
+ console.error(` Message: ${error.message}`);
1131
+ if (error.stack) {
1132
+ console.error(` Stack: ${error.stack}`);
1133
+ }
1134
+ }
1135
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
703
1136
  window.location.reload();
704
1137
  }
705
1138
  })();