@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.cjs CHANGED
@@ -111,22 +111,158 @@ function matchRouteClient(pathWithSearch, routes) {
111
111
  }
112
112
 
113
113
  // modules/runtime/client/metadata.ts
114
+ function getOrCreateMeta(selector, attributes) {
115
+ let meta = document.querySelector(selector);
116
+ if (!meta) {
117
+ meta = document.createElement("meta");
118
+ if (attributes.name) meta.name = attributes.name;
119
+ if (attributes.property) meta.setAttribute("property", attributes.property);
120
+ if (attributes.httpEquiv) meta.httpEquiv = attributes.httpEquiv;
121
+ document.head.appendChild(meta);
122
+ }
123
+ return meta;
124
+ }
125
+ function getOrCreateLink(rel, href) {
126
+ const selector = `link[rel="${rel}"]`;
127
+ let link = document.querySelector(selector);
128
+ if (!link) {
129
+ link = document.createElement("link");
130
+ link.rel = rel;
131
+ link.href = href;
132
+ document.head.appendChild(link);
133
+ } else {
134
+ link.href = href;
135
+ }
136
+ return link;
137
+ }
114
138
  function applyMetadata(md) {
115
139
  if (!md) return;
116
140
  if (md.title) {
117
141
  document.title = md.title;
118
142
  }
119
143
  if (md.description) {
120
- let meta = document.querySelector(
121
- 'meta[name="description"]'
122
- );
123
- if (!meta) {
124
- meta = document.createElement("meta");
125
- meta.name = "description";
126
- document.head.appendChild(meta);
127
- }
144
+ const meta = getOrCreateMeta('meta[name="description"]', { name: "description" });
128
145
  meta.content = md.description;
129
146
  }
147
+ if (md.robots) {
148
+ const meta = getOrCreateMeta('meta[name="robots"]', { name: "robots" });
149
+ meta.content = md.robots;
150
+ }
151
+ if (md.themeColor) {
152
+ const meta = getOrCreateMeta('meta[name="theme-color"]', { name: "theme-color" });
153
+ meta.content = md.themeColor;
154
+ }
155
+ if (md.viewport) {
156
+ const meta = getOrCreateMeta('meta[name="viewport"]', { name: "viewport" });
157
+ meta.content = md.viewport;
158
+ }
159
+ if (md.canonical) {
160
+ getOrCreateLink("canonical", md.canonical);
161
+ }
162
+ if (md.openGraph) {
163
+ const og = md.openGraph;
164
+ if (og.title) {
165
+ const meta = getOrCreateMeta('meta[property="og:title"]', { property: "og:title" });
166
+ meta.content = og.title;
167
+ }
168
+ if (og.description) {
169
+ const meta = getOrCreateMeta('meta[property="og:description"]', { property: "og:description" });
170
+ meta.content = og.description;
171
+ }
172
+ if (og.type) {
173
+ const meta = getOrCreateMeta('meta[property="og:type"]', { property: "og:type" });
174
+ meta.content = og.type;
175
+ }
176
+ if (og.url) {
177
+ const meta = getOrCreateMeta('meta[property="og:url"]', { property: "og:url" });
178
+ meta.content = og.url;
179
+ }
180
+ if (og.image) {
181
+ if (typeof og.image === "string") {
182
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
183
+ meta.content = og.image;
184
+ } else {
185
+ const meta = getOrCreateMeta('meta[property="og:image"]', { property: "og:image" });
186
+ meta.content = og.image.url;
187
+ if (og.image.width) {
188
+ const metaWidth = getOrCreateMeta('meta[property="og:image:width"]', { property: "og:image:width" });
189
+ metaWidth.content = String(og.image.width);
190
+ }
191
+ if (og.image.height) {
192
+ const metaHeight = getOrCreateMeta('meta[property="og:image:height"]', { property: "og:image:height" });
193
+ metaHeight.content = String(og.image.height);
194
+ }
195
+ if (og.image.alt) {
196
+ const metaAlt = getOrCreateMeta('meta[property="og:image:alt"]', { property: "og:image:alt" });
197
+ metaAlt.content = og.image.alt;
198
+ }
199
+ }
200
+ }
201
+ if (og.siteName) {
202
+ const meta = getOrCreateMeta('meta[property="og:site_name"]', { property: "og:site_name" });
203
+ meta.content = og.siteName;
204
+ }
205
+ if (og.locale) {
206
+ const meta = getOrCreateMeta('meta[property="og:locale"]', { property: "og:locale" });
207
+ meta.content = og.locale;
208
+ }
209
+ }
210
+ if (md.twitter) {
211
+ const twitter = md.twitter;
212
+ if (twitter.card) {
213
+ const meta = getOrCreateMeta('meta[name="twitter:card"]', { name: "twitter:card" });
214
+ meta.content = twitter.card;
215
+ }
216
+ if (twitter.title) {
217
+ const meta = getOrCreateMeta('meta[name="twitter:title"]', { name: "twitter:title" });
218
+ meta.content = twitter.title;
219
+ }
220
+ if (twitter.description) {
221
+ const meta = getOrCreateMeta('meta[name="twitter:description"]', { name: "twitter:description" });
222
+ meta.content = twitter.description;
223
+ }
224
+ if (twitter.image) {
225
+ const meta = getOrCreateMeta('meta[name="twitter:image"]', { name: "twitter:image" });
226
+ meta.content = twitter.image;
227
+ }
228
+ if (twitter.imageAlt) {
229
+ const meta = getOrCreateMeta('meta[name="twitter:image:alt"]', { name: "twitter:image:alt" });
230
+ meta.content = twitter.imageAlt;
231
+ }
232
+ if (twitter.site) {
233
+ const meta = getOrCreateMeta('meta[name="twitter:site"]', { name: "twitter:site" });
234
+ meta.content = twitter.site;
235
+ }
236
+ if (twitter.creator) {
237
+ const meta = getOrCreateMeta('meta[name="twitter:creator"]', { name: "twitter:creator" });
238
+ meta.content = twitter.creator;
239
+ }
240
+ }
241
+ if (md.metaTags && Array.isArray(md.metaTags)) {
242
+ md.metaTags.forEach((tag) => {
243
+ let selector = "";
244
+ if (tag.name) {
245
+ selector = `meta[name="${tag.name}"]`;
246
+ } else if (tag.property) {
247
+ selector = `meta[property="${tag.property}"]`;
248
+ } else if (tag.httpEquiv) {
249
+ selector = `meta[http-equiv="${tag.httpEquiv}"]`;
250
+ }
251
+ if (selector) {
252
+ const meta = getOrCreateMeta(selector, {
253
+ name: tag.name,
254
+ property: tag.property,
255
+ httpEquiv: tag.httpEquiv
256
+ });
257
+ meta.content = tag.content;
258
+ }
259
+ });
260
+ }
261
+ if (md.links && Array.isArray(md.links)) {
262
+ md.links.forEach((link) => {
263
+ getOrCreateLink(link.rel, link.href);
264
+ });
265
+ }
130
266
  }
131
267
 
132
268
  // modules/runtime/client/AppShell.tsx
@@ -343,10 +479,15 @@ async function handleErrorRoute(nextUrl, json, errorRoute, setState) {
343
479
  });
344
480
  return true;
345
481
  } catch (loadError) {
346
- console.error(
347
- "[client] Error loading error route components:",
348
- loadError
349
- );
482
+ console.error("\n\u274C [client] Error loading error route components:");
483
+ console.error(loadError);
484
+ if (loadError instanceof Error) {
485
+ console.error(` Message: ${loadError.message}`);
486
+ if (loadError.stack) {
487
+ console.error(` Stack: ${loadError.stack.split("\n").slice(0, 3).join("\n ")}`);
488
+ }
489
+ }
490
+ console.error("\u{1F4A1} Falling back to full page reload\n");
350
491
  window.location.href = nextUrl;
351
492
  return false;
352
493
  }
@@ -451,7 +592,11 @@ async function handleNormalRoute(nextUrl, json, routes, setState) {
451
592
  searchParams: Object.fromEntries(url.searchParams.entries())
452
593
  };
453
594
  setRouterData(routerData);
454
- const components = await matched.route.load();
595
+ const prefetched = prefetchedRoutes.get(matched.route);
596
+ const components = prefetched ? await prefetched : await matched.route.load();
597
+ if (!prefetched) {
598
+ prefetchedRoutes.set(matched.route, Promise.resolve(components));
599
+ }
455
600
  window.scrollTo({
456
601
  top: 0,
457
602
  behavior: "smooth"
@@ -490,7 +635,7 @@ async function navigate(nextUrl, handlers, options) {
490
635
  }
491
636
  }
492
637
  if (!ok) {
493
- if (json && json.redirect) {
638
+ if (json?.redirect) {
494
639
  window.location.href = json.redirect.destination;
495
640
  return;
496
641
  }
@@ -511,6 +656,47 @@ async function navigate(nextUrl, handlers, options) {
511
656
  window.location.href = nextUrl;
512
657
  }
513
658
  }
659
+ var prefetchedRoutes = /* @__PURE__ */ new WeakMap();
660
+ function prefetchRoute(url, routes, notFoundRoute) {
661
+ const [pathname] = url.split("?");
662
+ const matched = matchRouteClient(pathname, routes);
663
+ if (!matched) {
664
+ if (notFoundRoute) {
665
+ const existing2 = prefetchedRoutes.get(notFoundRoute);
666
+ if (!existing2) {
667
+ const promise = notFoundRoute.load();
668
+ prefetchedRoutes.set(notFoundRoute, promise);
669
+ }
670
+ }
671
+ return;
672
+ }
673
+ const existing = prefetchedRoutes.get(matched.route);
674
+ if (!existing) {
675
+ const promise = matched.route.load();
676
+ prefetchedRoutes.set(matched.route, promise);
677
+ }
678
+ }
679
+ function createHoverHandler(routes, notFoundRoute) {
680
+ return function handleHover(ev) {
681
+ try {
682
+ const target = ev.target;
683
+ if (!target) return;
684
+ const anchor = target.closest("a[href]");
685
+ if (!anchor) return;
686
+ const href = anchor.getAttribute("href");
687
+ if (!href) return;
688
+ if (href.startsWith("#")) return;
689
+ const url = new URL(href, window.location.href);
690
+ if (url.origin !== window.location.origin) return;
691
+ if (anchor.target && anchor.target !== "_self") return;
692
+ const nextUrl = url.pathname + url.search;
693
+ const currentUrl = window.location.pathname + window.location.search;
694
+ if (nextUrl === currentUrl) return;
695
+ prefetchRoute(nextUrl, routes, notFoundRoute);
696
+ } catch (error) {
697
+ }
698
+ };
699
+ }
514
700
  function createClickHandler(navigate2) {
515
701
  return function handleClick(ev) {
516
702
  try {
@@ -621,17 +807,20 @@ function AppShell({
621
807
  }
622
808
  const handleClick = createClickHandler(handleNavigateInternal);
623
809
  const handlePopState = createPopStateHandler(handleNavigateInternal);
810
+ const handleHover = createHoverHandler(routes, notFoundRoute);
624
811
  window.addEventListener("click", handleClick, false);
625
812
  window.addEventListener("popstate", handlePopState, false);
813
+ window.addEventListener("mouseover", handleHover, false);
626
814
  return () => {
627
815
  isMounted = false;
628
816
  window.removeEventListener("click", handleClick, false);
629
817
  window.removeEventListener("popstate", handlePopState, false);
818
+ window.removeEventListener("mouseover", handleHover, false);
630
819
  };
631
- }, []);
820
+ }, [routes, notFoundRoute]);
632
821
  (0, import_react2.useEffect)(() => {
633
822
  const handleDataRefresh = () => {
634
- const freshData = window?.__FW_DATA__;
823
+ const freshData = window[WINDOW_DATA_KEY];
635
824
  if (!freshData) return;
636
825
  const currentPathname = window.location.pathname;
637
826
  const freshPathname = freshData.pathname;
@@ -658,6 +847,91 @@ function AppShell({
658
847
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterView, { state }, routeKey) });
659
848
  }
660
849
 
850
+ // modules/runtime/client/hot-reload.ts
851
+ function setupHotReload() {
852
+ const nodeEnv = process.env.NODE_ENV || "production";
853
+ const isDev = nodeEnv === "development";
854
+ console.log(`[hot-reload] NODE_ENV: ${nodeEnv}, isDev: ${isDev}`);
855
+ if (!isDev) {
856
+ console.log("[hot-reload] Skipping hot reload setup (not in development mode)");
857
+ return;
858
+ }
859
+ console.log("[hot-reload] Setting up hot reload client...");
860
+ let eventSource = null;
861
+ let reloadTimeout = null;
862
+ let reconnectTimeout = null;
863
+ let reconnectAttempts = 0;
864
+ const MAX_RECONNECT_ATTEMPTS = 10;
865
+ const RECONNECT_DELAY = 1e3;
866
+ const RELOAD_DELAY = 100;
867
+ function connect() {
868
+ try {
869
+ if (eventSource) {
870
+ console.log("[hot-reload] Closing existing EventSource connection");
871
+ eventSource.close();
872
+ }
873
+ const endpoint = "/__fw/hot";
874
+ eventSource = new EventSource(endpoint);
875
+ eventSource.addEventListener("ping", (event) => {
876
+ if ("data" in event) {
877
+ console.log("[hot-reload] \u2705 Connected to hot reload server");
878
+ }
879
+ reconnectAttempts = 0;
880
+ });
881
+ eventSource.addEventListener("message", (event) => {
882
+ const data = event.data;
883
+ if (data && typeof data === "string" && data.startsWith("reload:")) {
884
+ const filePath = data.slice(7);
885
+ console.log(`[hot-reload] \u{1F4DD} File changed: ${filePath}, reloading...`);
886
+ if (reloadTimeout) {
887
+ clearTimeout(reloadTimeout);
888
+ }
889
+ reloadTimeout = setTimeout(() => {
890
+ try {
891
+ window.location.reload();
892
+ } catch (error) {
893
+ console.error("[hot-reload] \u274C Error reloading page:", error);
894
+ setTimeout(() => window.location.reload(), 100);
895
+ }
896
+ }, RELOAD_DELAY);
897
+ }
898
+ });
899
+ eventSource.onopen = () => {
900
+ reconnectAttempts = 0;
901
+ };
902
+ eventSource.onerror = (error) => {
903
+ const states = ["CONNECTING", "OPEN", "CLOSED"];
904
+ const state = states[eventSource?.readyState ?? 0] || "UNKNOWN";
905
+ if (eventSource?.readyState === EventSource.CONNECTING) {
906
+ console.log("[hot-reload] \u23F3 Still connecting...");
907
+ return;
908
+ } else if (eventSource?.readyState === EventSource.OPEN) {
909
+ console.warn("[hot-reload] \u26A0\uFE0F Connection error (but connection is open):", error);
910
+ } else {
911
+ console.warn(`[hot-reload] \u274C Connection closed (readyState: ${state})`);
912
+ if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
913
+ reconnectAttempts++;
914
+ const delay = RECONNECT_DELAY * reconnectAttempts;
915
+ if (reconnectTimeout) {
916
+ clearTimeout(reconnectTimeout);
917
+ }
918
+ reconnectTimeout = setTimeout(() => {
919
+ console.log(`[hot-reload] \u{1F504} Reconnecting... (attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
920
+ connect();
921
+ }, delay);
922
+ } else {
923
+ console.error("[hot-reload] \u274C Max reconnect attempts reached. Please refresh the page manually.");
924
+ }
925
+ }
926
+ };
927
+ } catch (error) {
928
+ console.error("[hot-reload] \u274C Failed to create EventSource:", error);
929
+ console.error("[hot-reload] EventSource may not be supported in this browser.");
930
+ }
931
+ }
932
+ connect();
933
+ }
934
+
661
935
  // modules/runtime/client/bootstrap.tsx
662
936
  var import_jsx_runtime3 = require("react/jsx-runtime");
663
937
  async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute, errorRoute) {
@@ -699,101 +973,91 @@ async function loadInitialRoute(initialUrl, initialData, routes, notFoundRoute,
699
973
  props: initialData?.props ?? {}
700
974
  };
701
975
  }
702
- function setupHotReload() {
703
- const nodeEnv = typeof process !== "undefined" && process.env?.NODE_ENV || "production";
704
- const isDev = nodeEnv !== "production";
705
- if (!isDev) {
706
- return;
976
+ function initializeRouterData(initialUrl, initialData) {
977
+ let routerData = getRouterData();
978
+ if (!routerData) {
979
+ const url = new URL(initialUrl, window.location.origin);
980
+ routerData = {
981
+ pathname: url.pathname,
982
+ params: initialData?.params || {},
983
+ searchParams: Object.fromEntries(url.searchParams.entries())
984
+ };
985
+ setRouterData(routerData);
707
986
  }
987
+ }
988
+ async function hydrateInitialRoute(container, initialUrl, initialData, routes, notFoundRoute, errorRoute) {
708
989
  try {
709
- console.log("[hot-reload] Attempting to connect to /__fw/hot...");
710
- const eventSource = new EventSource("/__fw/hot");
711
- let reloadTimeout = null;
712
- eventSource.addEventListener("message", (event) => {
713
- const data = event.data;
714
- if (data && data.startsWith("reload:")) {
715
- const filePath = data.slice(7);
716
- console.log(`[hot-reload] File changed: ${filePath}`);
717
- if (reloadTimeout) {
718
- clearTimeout(reloadTimeout);
719
- }
720
- reloadTimeout = setTimeout(() => {
721
- console.log("[hot-reload] Reloading page...");
722
- window.location.reload();
723
- }, 500);
724
- }
725
- });
726
- eventSource.addEventListener("ping", () => {
727
- console.log("[hot-reload] \u2713 Connected to hot reload server");
728
- });
729
- eventSource.onopen = () => {
730
- console.log("[hot-reload] \u2713 SSE connection opened");
731
- };
732
- eventSource.onerror = (error) => {
733
- const states = ["CONNECTING", "OPEN", "CLOSED"];
734
- const state = states[eventSource.readyState] || "UNKNOWN";
735
- if (eventSource.readyState === EventSource.CONNECTING) {
736
- console.log("[hot-reload] Connecting...");
737
- } else if (eventSource.readyState === EventSource.OPEN) {
738
- console.warn("[hot-reload] Connection error (but connection is open):", error);
739
- } else {
740
- console.log("[hot-reload] Connection closed (readyState:", state, ")");
990
+ const initialState = await loadInitialRoute(
991
+ initialUrl,
992
+ initialData,
993
+ routes,
994
+ notFoundRoute,
995
+ errorRoute
996
+ );
997
+ if (initialData?.metadata) {
998
+ try {
999
+ applyMetadata(initialData.metadata);
1000
+ } catch (metadataError) {
1001
+ console.warn("[client] Error applying metadata:", metadataError);
741
1002
  }
742
- };
1003
+ }
1004
+ (0, import_client.hydrateRoot)(
1005
+ container,
1006
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1007
+ AppShell,
1008
+ {
1009
+ initialState,
1010
+ routes,
1011
+ notFoundRoute,
1012
+ errorRoute
1013
+ }
1014
+ )
1015
+ );
743
1016
  } catch (error) {
744
- console.log("[hot-reload] EventSource not supported or error:", error);
1017
+ console.error(
1018
+ "[client] Error loading initial route components for",
1019
+ initialUrl,
1020
+ error
1021
+ );
1022
+ throw error;
745
1023
  }
746
1024
  }
747
1025
  function bootstrapClient(routes, notFoundRoute, errorRoute = null) {
748
- console.log("[client] Bootstrap starting, setting up hot reload...");
749
1026
  setupHotReload();
750
- (async function bootstrap() {
751
- const container = document.getElementById(APP_CONTAINER_ID);
752
- const initialData = getWindowData();
753
- if (!container) {
754
- console.error(`Container #${APP_CONTAINER_ID} not found for hydration`);
755
- return;
756
- }
757
- const initialUrl = window.location.pathname + window.location.search;
758
- let routerData = getRouterData();
759
- if (!routerData) {
760
- const url = new URL(initialUrl, window.location.origin);
761
- routerData = {
762
- pathname: url.pathname,
763
- params: initialData?.params || {},
764
- searchParams: Object.fromEntries(url.searchParams.entries())
765
- };
766
- setRouterData(routerData);
767
- }
1027
+ (async () => {
768
1028
  try {
769
- const initialState = await loadInitialRoute(
1029
+ const container = document.getElementById(APP_CONTAINER_ID);
1030
+ if (!container) {
1031
+ console.error(`
1032
+ \u274C [client] Hydration failed: Container #${APP_CONTAINER_ID} not found`);
1033
+ console.error("\u{1F4A1} This usually means:");
1034
+ console.error(" \u2022 The HTML structure doesn't match what React expects");
1035
+ console.error(" \u2022 The container was removed before hydration");
1036
+ console.error(" \u2022 There's a mismatch between SSR and client HTML\n");
1037
+ return;
1038
+ }
1039
+ const initialData = getWindowData();
1040
+ const initialUrl = window.location.pathname + window.location.search;
1041
+ initializeRouterData(initialUrl, initialData);
1042
+ await hydrateInitialRoute(
1043
+ container,
770
1044
  initialUrl,
771
1045
  initialData,
772
1046
  routes,
773
1047
  notFoundRoute,
774
1048
  errorRoute
775
1049
  );
776
- if (initialData?.metadata) {
777
- applyMetadata(initialData.metadata);
778
- }
779
- (0, import_client.hydrateRoot)(
780
- container,
781
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
782
- AppShell,
783
- {
784
- initialState,
785
- routes,
786
- notFoundRoute,
787
- errorRoute
788
- }
789
- )
790
- );
791
1050
  } catch (error) {
792
- console.error(
793
- "[client] Error loading initial route components for",
794
- initialUrl,
795
- error
796
- );
1051
+ console.error("\n\u274C [client] Fatal error during bootstrap:");
1052
+ console.error(error);
1053
+ if (error instanceof Error) {
1054
+ console.error("\nError details:");
1055
+ console.error(` Message: ${error.message}`);
1056
+ if (error.stack) {
1057
+ console.error(` Stack: ${error.stack}`);
1058
+ }
1059
+ }
1060
+ console.error("\n\u{1F4A1} Attempting page reload to recover...\n");
797
1061
  window.location.reload();
798
1062
  }
799
1063
  })();