@lolyjs/core 0.2.0-alpha.14 → 0.2.0-alpha.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/runtime.js CHANGED
@@ -85,22 +85,158 @@ function matchRouteClient(pathWithSearch, routes) {
85
85
  }
86
86
 
87
87
  // modules/runtime/client/metadata.ts
88
+ function getOrCreateMeta(selector, attributes) {
89
+ let meta = document.querySelector(selector);
90
+ if (!meta) {
91
+ meta = document.createElement("meta");
92
+ if (attributes.name) meta.name = attributes.name;
93
+ if (attributes.property) meta.setAttribute("property", attributes.property);
94
+ if (attributes.httpEquiv) meta.httpEquiv = attributes.httpEquiv;
95
+ document.head.appendChild(meta);
96
+ }
97
+ return meta;
98
+ }
99
+ function getOrCreateLink(rel, href) {
100
+ const selector = `link[rel="${rel}"]`;
101
+ let link = document.querySelector(selector);
102
+ if (!link) {
103
+ link = document.createElement("link");
104
+ link.rel = rel;
105
+ link.href = href;
106
+ document.head.appendChild(link);
107
+ } else {
108
+ link.href = href;
109
+ }
110
+ return link;
111
+ }
88
112
  function applyMetadata(md) {
89
113
  if (!md) return;
90
114
  if (md.title) {
91
115
  document.title = md.title;
92
116
  }
93
117
  if (md.description) {
94
- let meta = document.querySelector(
95
- 'meta[name="description"]'
96
- );
97
- if (!meta) {
98
- meta = document.createElement("meta");
99
- meta.name = "description";
100
- document.head.appendChild(meta);
101
- }
118
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
102
119
  meta.content = md.description;
103
120
  }
121
+ if (md.robots) {
122
+ const meta = getOrCreateMeta('meta[name="robots"]', { name: "robots" });
123
+ meta.content = md.robots;
124
+ }
125
+ if (md.themeColor) {
126
+ const meta = getOrCreateMeta('meta[name="theme-color"]', { name: "theme-color" });
127
+ meta.content = md.themeColor;
128
+ }
129
+ if (md.viewport) {
130
+ const meta = getOrCreateMeta('meta[name="viewport"]', { name: "viewport" });
131
+ meta.content = md.viewport;
132
+ }
133
+ if (md.canonical) {
134
+ getOrCreateLink("canonical", md.canonical);
135
+ }
136
+ if (md.openGraph) {
137
+ const og = md.openGraph;
138
+ if (og.title) {
139
+ const meta = getOrCreateMeta('meta[property="og:title"]', { property: "og:title" });
140
+ meta.content = og.title;
141
+ }
142
+ if (og.description) {
143
+ const meta = getOrCreateMeta('meta[property="og:description"]', { property: "og:description" });
144
+ meta.content = og.description;
145
+ }
146
+ if (og.type) {
147
+ const meta = getOrCreateMeta('meta[property="og:type"]', { property: "og:type" });
148
+ meta.content = og.type;
149
+ }
150
+ if (og.url) {
151
+ const meta = getOrCreateMeta('meta[property="og:url"]', { property: "og:url" });
152
+ meta.content = og.url;
153
+ }
154
+ if (og.image) {
155
+ if (typeof og.image === "string") {
156
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
157
+ meta.content = og.image;
158
+ } else {
159
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
160
+ meta.content = og.image.url;
161
+ if (og.image.width) {
162
+ const metaWidth = getOrCreateMeta('meta[property="og:image:width"]', { property: "og:image:width" });
163
+ metaWidth.content = String(og.image.width);
164
+ }
165
+ if (og.image.height) {
166
+ const metaHeight = getOrCreateMeta('meta[property="og:image:height"]', { property: "og:image:height" });
167
+ metaHeight.content = String(og.image.height);
168
+ }
169
+ if (og.image.alt) {
170
+ const metaAlt = getOrCreateMeta('meta[property="og:image:alt"]', { property: "og:image:alt" });
171
+ metaAlt.content = og.image.alt;
172
+ }
173
+ }
174
+ }
175
+ if (og.siteName) {
176
+ const meta = getOrCreateMeta('meta[property="og:site_name"]', { property: "og:site_name" });
177
+ meta.content = og.siteName;
178
+ }
179
+ if (og.locale) {
180
+ const meta = getOrCreateMeta('meta[property="og:locale"]', { property: "og:locale" });
181
+ meta.content = og.locale;
182
+ }
183
+ }
184
+ if (md.twitter) {
185
+ const twitter = md.twitter;
186
+ if (twitter.card) {
187
+ const meta = getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" });
188
+ meta.content = twitter.card;
189
+ }
190
+ if (twitter.title) {
191
+ const meta = getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" });
192
+ meta.content = twitter.title;
193
+ }
194
+ if (twitter.description) {
195
+ const meta = getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" });
196
+ meta.content = twitter.description;
197
+ }
198
+ if (twitter.image) {
199
+ const meta = getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" });
200
+ meta.content = twitter.image;
201
+ }
202
+ if (twitter.imageAlt) {
203
+ const meta = getOrCreateMeta('meta[name="twitter:image:alt"]', { name: "twitter:image:alt" });
204
+ meta.content = twitter.imageAlt;
205
+ }
206
+ if (twitter.site) {
207
+ const meta = getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" });
208
+ meta.content = twitter.site;
209
+ }
210
+ if (twitter.creator) {
211
+ const meta = getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" });
212
+ meta.content = twitter.creator;
213
+ }
214
+ }
215
+ if (md.metaTags && Array.isArray(md.metaTags)) {
216
+ md.metaTags.forEach((tag) => {
217
+ let selector = "";
218
+ if (tag.name) {
219
+ selector = `meta[name="${tag.name}"]`;
220
+ } else if (tag.property) {
221
+ selector = `meta[property="${tag.property}"]`;
222
+ } else if (tag.httpEquiv) {
223
+ selector = `meta[http-equiv="${tag.httpEquiv}"]`;
224
+ }
225
+ if (selector) {
226
+ const meta = getOrCreateMeta(selector, {
227
+ name: tag.name,
228
+ property: tag.property,
229
+ httpEquiv: tag.httpEquiv
230
+ });
231
+ meta.content = tag.content;
232
+ }
233
+ });
234
+ }
235
+ if (md.links && Array.isArray(md.links)) {
236
+ md.links.forEach((link) => {
237
+ getOrCreateLink(link.rel, link.href);
238
+ });
239
+ }
104
240
  }
105
241
 
106
242
  // modules/runtime/client/AppShell.tsx
@@ -317,10 +453,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
317
453
  });
318
454
  return true;
319
455
  } catch (loadError) {
320
- console.error(
321
- "[client] Error loading error route components:",
322
- loadError
323
- );
456
+ console.error("\n\u274C [client] Error loading error route components:");
457
+ console.error(loadError);
458
+ if (loadError instanceof Error) {
459
+ console.error(` Message: ${loadError.message}`);
460
+ if (loadError.stack) {
461
+ console.error(` Stack: ${loadError.stack.split("\n").slice(0, 3).join("\n ")}`);
462
+ }
463
+ }
464
+ console.error("\u{1F4A1} Falling back to full page reload\n");
324
465
  window.location.href = nextUrl;
325
466
  return false;
326
467
  }
@@ -425,7 +566,11 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
425
566
  searchParams: Object.fromEntries(url.searchParams.entries())
426
567
  };
427
568
  setRouterData(routerData);
428
- const components = await matched.route.load();
569
+ const prefetched = prefetchedRoutes.get(matched.route);
570
+ const components = prefetched ? await prefetched : await matched.route.load();
571
+ if (!prefetched) {
572
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
573
+ }
429
574
  window.scrollTo({
430
575
  top: 0,
431
576
  behavior: "smooth"
@@ -464,7 +609,7 @@ async function navigate(nextUrl, handlers, options) {
464
609
  }
465
610
  }
466
611
  if (!ok) {
467
- if (json && json.redirect) {
612
+ if (json?.redirect) {
468
613
  window.location.href = json.redirect.destination;
469
614
  return;
470
615
  }
@@ -485,6 +630,47 @@ async function navigate(nextUrl, handlers, options) {
485
630
  window.location.href = nextUrl;
486
631
  }
487
632
  }
633
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
634
+ function prefetchRoute(url, routes, notFoundRoute) {
635
+ const [pathname] = url.split("?");
636
+ const matched = matchRouteClient(pathname, routes);
637
+ if (!matched) {
638
+ if (notFoundRoute) {
639
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
640
+ if (!existing2) {
641
+ const promise = notFoundRoute.load();
642
+ prefetchedRoutes.set(notFoundRoute, promise);
643
+ }
644
+ }
645
+ return;
646
+ }
647
+ const existing = prefetchedRoutes.get(matched.route);
648
+ if (!existing) {
649
+ const promise = matched.route.load();
650
+ prefetchedRoutes.set(matched.route, promise);
651
+ }
652
+ }
653
+ function createHoverHandler(routes, notFoundRoute) {
654
+ return function handleHover(ev) {
655
+ try {
656
+ const target = ev.target;
657
+ if (!target) return;
658
+ const anchor = target.closest("a[href]");
659
+ if (!anchor) return;
660
+ const href = anchor.getAttribute("href");
661
+ if (!href) return;
662
+ if (href.startsWith("#")) return;
663
+ const url = new URL(href, window.location.href);
664
+ if (url.origin !== window.location.origin) return;
665
+ if (anchor.target && anchor.target !== "_self") return;
666
+ const nextUrl = url.pathname + url.search;
667
+ const currentUrl = window.location.pathname + window.location.search;
668
+ if (nextUrl === currentUrl) return;
669
+ prefetchRoute(nextUrl, routes, notFoundRoute);
670
+ } catch (error) {
671
+ }
672
+ };
673
+ }
488
674
  function createClickHandler(navigate2) {
489
675
  return function handleClick(ev) {
490
676
  try {
@@ -595,17 +781,20 @@ function AppShell({
595
781
  }
596
782
  const handleClick = createClickHandler(handleNavigateInternal);
597
783
  const handlePopState = createPopStateHandler(handleNavigateInternal);
784
+ const handleHover = createHoverHandler(routes, notFoundRoute);
598
785
  window.addEventListener("click", handleClick, false);
599
786
  window.addEventListener("popstate", handlePopState, false);
787
+ window.addEventListener("mouseover", handleHover, false);
600
788
  return () => {
601
789
  isMounted = false;
602
790
  window.removeEventListener("click", handleClick, false);
603
791
  window.removeEventListener("popstate", handlePopState, false);
792
+ window.removeEventListener("mouseover", handleHover, false);
604
793
  };
605
- }, []);
794
+ }, [routes, notFoundRoute]);
606
795
  useEffect(() => {
607
796
  const handleDataRefresh = () => {
608
- const freshData = window?.__FW_DATA__;
797
+ const freshData = window[WINDOW_DATA_KEY];
609
798
  if (!freshData) return;
610
799
  const currentPathname = window.location.pathname;
611
800
  const freshPathname = freshData.pathname;
@@ -632,6 +821,91 @@ function AppShell({
632
821
  return /* @__PURE__ */ jsx2(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ jsx2(RouterView, { state }, routeKey) });
633
822
  }
634
823
 
824
+ // modules/runtime/client/hot-reload.ts
825
+ function setupHotReload() {
826
+ const nodeEnv = process.env.NODE_ENV || "production";
827
+ const isDev = nodeEnv === "development";
828
+ console.log(`[hot-reload] NODE_ENV: ${nodeEnv}, isDev: ${isDev}`);
829
+ if (!isDev) {
830
+ console.log("[hot-reload] Skipping hot reload setup (not in development mode)");
831
+ return;
832
+ }
833
+ console.log("[hot-reload] Setting up hot reload client...");
834
+ let eventSource = null;
835
+ let reloadTimeout = null;
836
+ let reconnectTimeout = null;
837
+ let reconnectAttempts = 0;
838
+ const MAX_RECONNECT_ATTEMPTS = 10;
839
+ const RECONNECT_DELAY = 1e3;
840
+ const RELOAD_DELAY = 100;
841
+ function connect() {
842
+ try {
843
+ if (eventSource) {
844
+ console.log("[hot-reload] Closing existing EventSource connection");
845
+ eventSource.close();
846
+ }
847
+ const endpoint = "/__fw/hot";
848
+ eventSource = new EventSource(endpoint);
849
+ eventSource.addEventListener("ping", (event) => {
850
+ if ("data" in event) {
851
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
852
+ }
853
+ reconnectAttempts = 0;
854
+ });
855
+ eventSource.addEventListener("message", (event) => {
856
+ const data = event.data;
857
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
858
+ const filePath = data.slice(7);
859
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
860
+ if (reloadTimeout) {
861
+ clearTimeout(reloadTimeout);
862
+ }
863
+ reloadTimeout = setTimeout(() => {
864
+ try {
865
+ window.location.reload();
866
+ } catch (error) {
867
+ console.error("[hot-reload] \u274C Error reloading page:", error);
868
+ setTimeout(() => window.location.reload(), 100);
869
+ }
870
+ }, RELOAD_DELAY);
871
+ }
872
+ });
873
+ eventSource.onopen = () => {
874
+ reconnectAttempts = 0;
875
+ };
876
+ eventSource.onerror = (error) => {
877
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
878
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
879
+ if (eventSource?.readyState === EventSource.CONNECTING) {
880
+ console.log("[hot-reload] \u23F3 Still connecting...");
881
+ return;
882
+ } else if (eventSource?.readyState === EventSource.OPEN) {
883
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
884
+ } else {
885
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
886
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
887
+ reconnectAttempts++;
888
+ const delay = RECONNECT_DELAY * reconnectAttempts;
889
+ if (reconnectTimeout) {
890
+ clearTimeout(reconnectTimeout);
891
+ }
892
+ reconnectTimeout = setTimeout(() => {
893
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
894
+ connect();
895
+ }, delay);
896
+ } else {
897
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
898
+ }
899
+ }
900
+ };
901
+ } catch (error) {
902
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
903
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
904
+ }
905
+ }
906
+ connect();
907
+ }
908
+
635
909
  // modules/runtime/client/bootstrap.tsx
636
910
  import { jsx as jsx3 } from "react/jsx-runtime";
637
911
  async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute, errorRoute) {
@@ -673,101 +947,91 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
673
947
  props: initialData?.props ?? {}
674
948
  };
675
949
  }
676
- function setupHotReload() {
677
- const nodeEnv = typeof process !== "undefined" && process.env?.NODE_ENV || "production";
678
- const isDev = nodeEnv !== "production";
679
- if (!isDev) {
680
- return;
950
+ function initializeRouterData(initialUrl, initialData) {
951
+ let routerData = getRouterData();
952
+ if (!routerData) {
953
+ const url = new URL(initialUrl, window.location.origin);
954
+ routerData = {
955
+ pathname: url.pathname,
956
+ params: initialData?.params || {},
957
+ searchParams: Object.fromEntries(url.searchParams.entries())
958
+ };
959
+ setRouterData(routerData);
681
960
  }
961
+ }
962
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
682
963
  try {
683
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
684
- const eventSource = new EventSource("/__fw/hot");
685
- let reloadTimeout = null;
686
- eventSource.addEventListener("message", (event) => {
687
- const data = event.data;
688
- if (data && data.startsWith("reload:")) {
689
- const filePath = data.slice(7);
690
- console.log(`[hot-reload] File changed: ${filePath}`);
691
- if (reloadTimeout) {
692
- clearTimeout(reloadTimeout);
693
- }
694
- reloadTimeout = setTimeout(() => {
695
- console.log("[hot-reload] Reloading page...");
696
- window.location.reload();
697
- }, 500);
698
- }
699
- });
700
- eventSource.addEventListener("ping", () => {
701
- console.log("[hot-reload] \u2713 Connected to hot reload server");
702
- });
703
- eventSource.onopen = () => {
704
- console.log("[hot-reload] \u2713 SSE connection opened");
705
- };
706
- eventSource.onerror = (error) => {
707
- const states = ["CONNECTING", "OPEN", "CLOSED"];
708
- const state = states[eventSource.readyState] || "UNKNOWN";
709
- if (eventSource.readyState === EventSource.CONNECTING) {
710
- console.log("[hot-reload] Connecting...");
711
- } else if (eventSource.readyState === EventSource.OPEN) {
712
- console.warn("[hot-reload] Connection error (but connection is open):", error);
713
- } else {
714
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
964
+ const initialState = await loadInitialRoute(
965
+ initialUrl,
966
+ initialData,
967
+ routes,
968
+ notFoundRoute,
969
+ errorRoute
970
+ );
971
+ if (initialData?.metadata) {
972
+ try {
973
+ applyMetadata(initialData.metadata);
974
+ } catch (metadataError) {
975
+ console.warn("[client] Error applying metadata:", metadataError);
715
976
  }
716
- };
977
+ }
978
+ hydrateRoot(
979
+ container,
980
+ /* @__PURE__ */ jsx3(
981
+ AppShell,
982
+ {
983
+ initialState,
984
+ routes,
985
+ notFoundRoute,
986
+ errorRoute
987
+ }
988
+ )
989
+ );
717
990
  } catch (error) {
718
- console.log("[hot-reload] EventSource not supported or error:", error);
991
+ console.error(
992
+ "[client] Error loading initial route components for",
993
+ initialUrl,
994
+ error
995
+ );
996
+ throw error;
719
997
  }
720
998
  }
721
999
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
722
- console.log("[client] Bootstrap starting, setting up hot reload...");
723
1000
  setupHotReload();
724
- (async function bootstrap() {
725
- const container = document.getElementById(APP_CONTAINER_ID);
726
- const initialData = getWindowData();
727
- if (!container) {
728
- console.error(`Container #${APP_CONTAINER_ID} not found for hydration`);
729
- return;
730
- }
731
- const initialUrl = window.location.pathname + window.location.search;
732
- let routerData = getRouterData();
733
- if (!routerData) {
734
- const url = new URL(initialUrl, window.location.origin);
735
- routerData = {
736
- pathname: url.pathname,
737
- params: initialData?.params || {},
738
- searchParams: Object.fromEntries(url.searchParams.entries())
739
- };
740
- setRouterData(routerData);
741
- }
1001
+ (async () => {
742
1002
  try {
743
- const initialState = await loadInitialRoute(
1003
+ const container = document.getElementById(APP_CONTAINER_ID);
1004
+ if (!container) {
1005
+ console.error(`
1006
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID} not found`);
1007
+ console.error("\u{1F4A1} This usually means:");
1008
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
1009
+ console.error(" \u2022 The container was removed before hydration");
1010
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
1011
+ return;
1012
+ }
1013
+ const initialData = getWindowData();
1014
+ const initialUrl = window.location.pathname + window.location.search;
1015
+ initializeRouterData(initialUrl, initialData);
1016
+ await hydrateInitialRoute(
1017
+ container,
744
1018
  initialUrl,
745
1019
  initialData,
746
1020
  routes,
747
1021
  notFoundRoute,
748
1022
  errorRoute
749
1023
  );
750
- if (initialData?.metadata) {
751
- applyMetadata(initialData.metadata);
752
- }
753
- hydrateRoot(
754
- container,
755
- /* @__PURE__ */ jsx3(
756
- AppShell,
757
- {
758
- initialState,
759
- routes,
760
- notFoundRoute,
761
- errorRoute
762
- }
763
- )
764
- );
765
1024
  } catch (error) {
766
- console.error(
767
- "[client] Error loading initial route components for",
768
- initialUrl,
769
- error
770
- );
1025
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
1026
+ console.error(error);
1027
+ if (error instanceof Error) {
1028
+ console.error("\nError details:");
1029
+ console.error(` Message: ${error.message}`);
1030
+ if (error.stack) {
1031
+ console.error(` Stack: ${error.stack}`);
1032
+ }
1033
+ }
1034
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
771
1035
  window.location.reload();
772
1036
  }
773
1037
  })();