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

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.
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var hooks_exports = {};
22
22
  __export(hooks_exports, {
23
23
  useBroadcastChannel: () => useBroadcastChannel,
24
- usePageProps: () => usePageProps
24
+ usePageProps: () => usePageProps,
25
+ useRouter: () => useRouter
25
26
  });
26
27
  module.exports = __toCommonJS(hooks_exports);
27
28
 
@@ -78,9 +79,140 @@ function usePageProps() {
78
79
  }, []);
79
80
  return { params: state.params, props: state.props };
80
81
  }
82
+
83
+ // modules/react/hooks/useRouter/index.ts
84
+ var import_react4 = require("react");
85
+
86
+ // modules/runtime/client/RouterContext.tsx
87
+ var import_react3 = require("react");
88
+ var RouterContext = (0, import_react3.createContext)(null);
89
+
90
+ // modules/runtime/client/constants.ts
91
+ var WINDOW_DATA_KEY = "__FW_DATA__";
92
+
93
+ // modules/runtime/client/window-data.ts
94
+ function getWindowData() {
95
+ if (typeof window === "undefined") {
96
+ return null;
97
+ }
98
+ return window[WINDOW_DATA_KEY] ?? null;
99
+ }
100
+
101
+ // modules/react/hooks/useRouter/index.ts
102
+ function useRouter() {
103
+ const context = (0, import_react4.useContext)(RouterContext);
104
+ const navigate = context?.navigate;
105
+ const [routeData, setRouteData] = (0, import_react4.useState)(() => {
106
+ if (typeof window === "undefined") {
107
+ return {
108
+ pathname: "",
109
+ query: {},
110
+ params: {}
111
+ };
112
+ }
113
+ const data = getWindowData();
114
+ return {
115
+ pathname: data?.pathname || window.location.pathname,
116
+ query: parseQueryString(window.location.search),
117
+ params: data?.params || {}
118
+ };
119
+ });
120
+ (0, import_react4.useEffect)(() => {
121
+ if (typeof window === "undefined") return;
122
+ const handleDataRefresh = () => {
123
+ const data = getWindowData();
124
+ const currentPathname = window.location.pathname;
125
+ const currentSearch = window.location.search;
126
+ setRouteData({
127
+ pathname: data?.pathname || currentPathname,
128
+ query: parseQueryString(currentSearch),
129
+ params: data?.params || {}
130
+ });
131
+ };
132
+ window.addEventListener("fw-data-refresh", handleDataRefresh);
133
+ const handlePopState = () => {
134
+ handleDataRefresh();
135
+ };
136
+ window.addEventListener("popstate", handlePopState);
137
+ return () => {
138
+ window.removeEventListener("fw-data-refresh", handleDataRefresh);
139
+ window.removeEventListener("popstate", handlePopState);
140
+ };
141
+ }, []);
142
+ const push = (0, import_react4.useCallback)(
143
+ async (url, options) => {
144
+ const fullUrl = url.startsWith("/") ? url : `/${url}`;
145
+ if (!navigate) {
146
+ if (typeof window !== "undefined") {
147
+ window.location.href = fullUrl;
148
+ }
149
+ return;
150
+ }
151
+ if (typeof window !== "undefined") {
152
+ window.history.pushState({}, "", fullUrl);
153
+ }
154
+ await navigate(fullUrl, options);
155
+ },
156
+ [navigate]
157
+ );
158
+ const replace = (0, import_react4.useCallback)(
159
+ async (url, options) => {
160
+ const fullUrl = url.startsWith("/") ? url : `/${url}`;
161
+ if (!navigate) {
162
+ if (typeof window !== "undefined") {
163
+ window.location.replace(fullUrl);
164
+ }
165
+ return;
166
+ }
167
+ if (typeof window !== "undefined") {
168
+ window.history.replaceState({}, "", fullUrl);
169
+ }
170
+ await navigate(fullUrl, options);
171
+ },
172
+ [navigate]
173
+ );
174
+ const back = (0, import_react4.useCallback)(() => {
175
+ if (typeof window !== "undefined") {
176
+ window.history.back();
177
+ }
178
+ }, []);
179
+ const refresh = (0, import_react4.useCallback)(async () => {
180
+ const currentUrl = typeof window !== "undefined" ? window.location.pathname + window.location.search : routeData.pathname;
181
+ if (!navigate) {
182
+ if (typeof window !== "undefined") {
183
+ window.location.reload();
184
+ }
185
+ return;
186
+ }
187
+ await navigate(currentUrl, { revalidate: true });
188
+ }, [navigate, routeData.pathname]);
189
+ return {
190
+ push,
191
+ replace,
192
+ back,
193
+ refresh,
194
+ pathname: routeData.pathname,
195
+ query: routeData.query,
196
+ params: routeData.params
197
+ };
198
+ }
199
+ function parseQueryString(search) {
200
+ const params = {};
201
+ if (!search || search.length === 0) return params;
202
+ const queryString = search.startsWith("?") ? search.slice(1) : search;
203
+ const pairs = queryString.split("&");
204
+ for (const pair of pairs) {
205
+ const [key, value] = pair.split("=");
206
+ if (key) {
207
+ params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : "";
208
+ }
209
+ }
210
+ return params;
211
+ }
81
212
  // Annotate the CommonJS export names for ESM import in node:
82
213
  0 && (module.exports = {
83
214
  useBroadcastChannel,
84
- usePageProps
215
+ usePageProps,
216
+ useRouter
85
217
  });
86
218
  //# sourceMappingURL=hooks.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../modules/react/hooks/index.ts","../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/usePageProps/index.ts"],"sourcesContent":["export { useBroadcastChannel } from \"./useBroadcastChannel\";\r\nexport { usePageProps } from \"./usePageProps\";\r\n","import React, { useEffect, useState } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channel = new BroadcastChannel(channelName);\r\n\r\n useEffect(() => {\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n channel.close();\r\n };\r\n }, [channel]);\r\n\r\n const sendMessage = (msg: unknown) => {\r\n channel.postMessage(msg);\r\n };\r\n\r\n return { message, sendMessage };\r\n};\r\n","import React, { useEffect, useState } from \"react\";\r\n\r\n/**\r\n * Hook to access page props and route parameters.\r\n *\r\n * Reads initial data from window.__FW_DATA__ set during SSR.\r\n * Automatically updates when `revalidate()` is called.\r\n *\r\n * @template P - Type for page props (default: any)\r\n * @template T - Type for route params (default: any)\r\n * @returns Object containing params and props\r\n *\r\n * @example\r\n * // With props type only\r\n * const { props } = usePageProps<{ title: string }>();\r\n *\r\n * @example\r\n * // With both props and params types\r\n * const { props, params } = usePageProps<{ title: string }, { id: string }>();\r\n */\r\nexport function usePageProps<P = any, T = any>(): { params: T, props: P } {\r\n const [state, setState] = useState<{ params: T, props: P }>(() => {\r\n // Initialize with current data if available\r\n if (typeof window !== \"undefined\" && (window as any)?.__FW_DATA__) {\r\n const data = (window as any).__FW_DATA__;\r\n return {\r\n params: data.params || {} as T,\r\n props: data.props || {} as P,\r\n };\r\n }\r\n return {\r\n params: {} as T,\r\n props: {} as P,\r\n };\r\n });\r\n\r\n useEffect(() => {\r\n // Listen for data refresh events (from revalidate() or navigation)\r\n const handleDataRefresh = () => {\r\n if ((window as any)?.__FW_DATA__) {\r\n const data = (window as any).__FW_DATA__;\r\n setState({\r\n params: data.params || {} as T,\r\n props: data.props || {} as P,\r\n });\r\n }\r\n };\r\n\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n };\r\n }, []);\r\n\r\n return { params: state.params as T, props: state.props as P };\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAA2C;AAEpC,IAAM,sBAAsB,CAAC,gBAAwB;AAC1D,QAAM,CAAC,SAAS,UAAU,QAAI,uBAAS,IAAI;AAC3C,QAAM,UAAU,IAAI,iBAAiB,WAAW;AAEhD,8BAAU,MAAM;AACd,UAAM,gBAAgB,CAAC,UAAwB;AAC7C,iBAAW,MAAM,IAAI;AAAA,IACvB;AAEA,YAAQ,YAAY;AAGpB,WAAO,MAAM;AACX,cAAQ,MAAM;AAAA,IAChB;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,cAAc,CAAC,QAAiB;AACpC,YAAQ,YAAY,GAAG;AAAA,EACzB;AAEA,SAAO,EAAE,SAAS,YAAY;AAChC;;;ACxBA,IAAAA,gBAA2C;AAoBpC,SAAS,eAA0D;AACxE,QAAM,CAAC,OAAO,QAAQ,QAAI,wBAAkC,MAAM;AAEhE,QAAI,OAAO,WAAW,eAAgB,QAAgB,aAAa;AACjE,YAAM,OAAQ,OAAe;AAC7B,aAAO;AAAA,QACL,QAAQ,KAAK,UAAU,CAAC;AAAA,QACxB,OAAO,KAAK,SAAS,CAAC;AAAA,MACxB;AAAA,IACF;AACA,WAAO;AAAA,MACL,QAAQ,CAAC;AAAA,MACT,OAAO,CAAC;AAAA,IACV;AAAA,EACF,CAAC;AAED,+BAAU,MAAM;AAEd,UAAM,oBAAoB,MAAM;AAC9B,UAAK,QAAgB,aAAa;AAChC,cAAM,OAAQ,OAAe;AAC7B,iBAAS;AAAA,UACP,QAAQ,KAAK,UAAU,CAAC;AAAA,UACxB,OAAO,KAAK,SAAS,CAAC;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO,iBAAiB,mBAAmB,iBAAiB;AAE5D,WAAO,MAAM;AACX,aAAO,oBAAoB,mBAAmB,iBAAiB;AAAA,IACjE;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,QAAQ,MAAM,QAAa,OAAO,MAAM,MAAW;AAC9D;","names":["import_react"]}
1
+ {"version":3,"sources":["../../modules/react/hooks/index.ts","../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/usePageProps/index.ts","../../modules/react/hooks/useRouter/index.ts","../../modules/runtime/client/RouterContext.tsx","../../modules/runtime/client/constants.ts","../../modules/runtime/client/window-data.ts"],"sourcesContent":["export { useBroadcastChannel } from \"./useBroadcastChannel\";\r\nexport { usePageProps } from \"./usePageProps\";\r\nexport { useRouter } from \"./useRouter\";\r\nexport type { Router } from \"./useRouter\";\r\n","import React, { useEffect, useState } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channel = new BroadcastChannel(channelName);\r\n\r\n useEffect(() => {\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n channel.close();\r\n };\r\n }, [channel]);\r\n\r\n const sendMessage = (msg: unknown) => {\r\n channel.postMessage(msg);\r\n };\r\n\r\n return { message, sendMessage };\r\n};\r\n","import React, { useEffect, useState } from \"react\";\r\n\r\n/**\r\n * Hook to access page props and route parameters.\r\n *\r\n * Reads initial data from window.__FW_DATA__ set during SSR.\r\n * Automatically updates when `revalidate()` is called.\r\n *\r\n * @template P - Type for page props (default: any)\r\n * @template T - Type for route params (default: any)\r\n * @returns Object containing params and props\r\n *\r\n * @example\r\n * // With props type only\r\n * const { props } = usePageProps<{ title: string }>();\r\n *\r\n * @example\r\n * // With both props and params types\r\n * const { props, params } = usePageProps<{ title: string }, { id: string }>();\r\n */\r\nexport function usePageProps<P = any, T = any>(): { params: T, props: P } {\r\n const [state, setState] = useState<{ params: T, props: P }>(() => {\r\n // Initialize with current data if available\r\n if (typeof window !== \"undefined\" && (window as any)?.__FW_DATA__) {\r\n const data = (window as any).__FW_DATA__;\r\n return {\r\n params: data.params || {} as T,\r\n props: data.props || {} as P,\r\n };\r\n }\r\n return {\r\n params: {} as T,\r\n props: {} as P,\r\n };\r\n });\r\n\r\n useEffect(() => {\r\n // Listen for data refresh events (from revalidate() or navigation)\r\n const handleDataRefresh = () => {\r\n if ((window as any)?.__FW_DATA__) {\r\n const data = (window as any).__FW_DATA__;\r\n setState({\r\n params: data.params || {} as T,\r\n props: data.props || {} as P,\r\n });\r\n }\r\n };\r\n\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n };\r\n }, []);\r\n\r\n return { params: state.params as T, props: state.props as P };\r\n};\r\n","import { useState, useEffect, useCallback, useContext } from \"react\";\r\nimport { RouterContext } from \"../../../runtime/client/RouterContext\";\r\nimport { getWindowData } from \"../../../runtime/client/window-data\";\r\n\r\nexport interface Router {\r\n /**\r\n * Navigate to a new route.\r\n * @param url - The URL to navigate to (e.g., \"/about\" or \"/blog/[slug]\" with params)\r\n * @param options - Navigation options\r\n */\r\n push: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Replace the current route without adding to history.\r\n * @param url - The URL to navigate to\r\n * @param options - Navigation options\r\n */\r\n replace: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Go back in the browser history.\r\n */\r\n back: () => void;\r\n \r\n /**\r\n * Refresh the current route data by revalidating.\r\n */\r\n refresh: () => Promise<void>;\r\n \r\n /**\r\n * Current pathname (e.g., \"/blog/my-post\")\r\n */\r\n pathname: string;\r\n \r\n /**\r\n * Query parameters from the URL (e.g., ?id=123&name=test)\r\n */\r\n query: Record<string, string>;\r\n \r\n /**\r\n * Dynamic route parameters (e.g., { slug: \"my-post\" } for /blog/[slug])\r\n */\r\n params: Record<string, string>;\r\n}\r\n\r\n/**\r\n * Hook to access router functionality and current route information.\r\n * \r\n * Provides methods to navigate programmatically and access current route data.\r\n * \r\n * @returns Router object with navigation methods and route information\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const router = useRouter();\r\n * \r\n * const handleClick = () => {\r\n * router.push(\"/about\");\r\n * };\r\n * \r\n * return (\r\n * <div>\r\n * <p>Current path: {router.pathname}</p>\r\n * <p>Params: {JSON.stringify(router.params)}</p>\r\n * <button onClick={handleClick}>Go to About</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n * \r\n * @example\r\n * ```tsx\r\n * // Navigate with dynamic params\r\n * router.push(\"/blog/my-post\");\r\n * \r\n * // Replace current route\r\n * router.replace(\"/login\");\r\n * \r\n * // Refresh current route data\r\n * await router.refresh();\r\n * ```\r\n */\r\nexport function useRouter(): Router {\r\n // Try to get context, but don't throw if it's not available (SSR)\r\n const context = useContext(RouterContext);\r\n const navigate = context?.navigate;\r\n \r\n const [routeData, setRouteData] = useState(() => {\r\n // During SSR, return empty/default values\r\n if (typeof window === \"undefined\") {\r\n return {\r\n pathname: \"\",\r\n query: {},\r\n params: {},\r\n };\r\n }\r\n \r\n // On client, get data from window\r\n const data = getWindowData();\r\n return {\r\n pathname: data?.pathname || window.location.pathname,\r\n query: parseQueryString(window.location.search),\r\n params: data?.params || {},\r\n };\r\n });\r\n\r\n // Listen for route changes (only on client)\r\n useEffect(() => {\r\n if (typeof window === \"undefined\") return;\r\n \r\n const handleDataRefresh = () => {\r\n const data = getWindowData();\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n \r\n setRouteData({\r\n pathname: data?.pathname || currentPathname,\r\n query: parseQueryString(currentSearch),\r\n params: data?.params || {},\r\n });\r\n };\r\n\r\n // Listen for navigation events\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n \r\n // Also listen for popstate (browser back/forward)\r\n const handlePopState = () => {\r\n handleDataRefresh();\r\n };\r\n window.addEventListener(\"popstate\", handlePopState);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"popstate\", handlePopState);\r\n };\r\n }, []);\r\n\r\n const push = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n // During SSR or if context is not available, use window.location\r\n if (!navigate) {\r\n if (typeof window !== \"undefined\") {\r\n window.location.href = fullUrl;\r\n }\r\n return;\r\n }\r\n \r\n if (typeof window !== \"undefined\") {\r\n window.history.pushState({}, \"\", fullUrl);\r\n }\r\n await navigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const replace = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n // During SSR or if context is not available, use window.location\r\n if (!navigate) {\r\n if (typeof window !== \"undefined\") {\r\n window.location.replace(fullUrl);\r\n }\r\n return;\r\n }\r\n \r\n if (typeof window !== \"undefined\") {\r\n window.history.replaceState({}, \"\", fullUrl);\r\n }\r\n await navigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const back = useCallback(() => {\r\n if (typeof window !== \"undefined\") {\r\n window.history.back();\r\n }\r\n }, []);\r\n\r\n const refresh = useCallback(async () => {\r\n const currentUrl = typeof window !== \"undefined\" \r\n ? window.location.pathname + window.location.search \r\n : routeData.pathname;\r\n \r\n // During SSR or if context is not available, reload the page\r\n if (!navigate) {\r\n if (typeof window !== \"undefined\") {\r\n window.location.reload();\r\n }\r\n return;\r\n }\r\n \r\n await navigate(currentUrl, { revalidate: true });\r\n }, [navigate, routeData.pathname]);\r\n\r\n return {\r\n push,\r\n replace,\r\n back,\r\n refresh,\r\n pathname: routeData.pathname,\r\n query: routeData.query,\r\n params: routeData.params,\r\n };\r\n}\r\n\r\n/**\r\n * Parse query string into an object.\r\n * @param search - Query string (e.g., \"?id=123&name=test\")\r\n * @returns Object with query parameters\r\n */\r\nfunction parseQueryString(search: string): Record<string, string> {\r\n const params: Record<string, string> = {};\r\n if (!search || search.length === 0) return params;\r\n \r\n const queryString = search.startsWith(\"?\") ? search.slice(1) : search;\r\n const pairs = queryString.split(\"&\");\r\n \r\n for (const pair of pairs) {\r\n const [key, value] = pair.split(\"=\");\r\n if (key) {\r\n params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : \"\";\r\n }\r\n }\r\n \r\n return params;\r\n}\r\n","import { createContext, useContext } from \"react\";\r\n\r\nexport type NavigateFunction = (\r\n url: string,\r\n options?: { revalidate?: boolean; replace?: boolean }\r\n) => Promise<void>;\r\n\r\nexport interface RouterContextValue {\r\n navigate: NavigateFunction;\r\n}\r\n\r\nexport const RouterContext = createContext<RouterContextValue | null>(null);\r\n\r\nexport function useRouterContext(): RouterContextValue {\r\n const context = useContext(RouterContext);\r\n if (!context) {\r\n throw new Error(\r\n \"useRouter must be used within a RouterProvider. Make sure you're using it inside a Loly app.\"\r\n );\r\n }\r\n return context;\r\n}\r\n","// Client-side constants (hardcoded to avoid alias resolution issues in Rspack)\r\nexport const WINDOW_DATA_KEY = \"__FW_DATA__\";\r\nexport const APP_CONTAINER_ID = \"__app\";\r\n\r\n","import { WINDOW_DATA_KEY } from \"./constants\";\r\nimport type { InitialData } from \"./types\";\r\n\r\nexport function getWindowData(): InitialData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return ((window as any)[WINDOW_DATA_KEY] as InitialData | undefined) ?? null;\r\n}\r\n\r\nexport function setWindowData(data: InitialData): void {\r\n (window as any)[WINDOW_DATA_KEY] = data;\r\n \r\n // Dispatch event for components to listen to (e.g., usePageProps, ThemeProvider)\r\n // This ensures components update when navigating in SPA mode\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function getCurrentTheme(): string | null {\r\n return getWindowData()?.theme ?? null;\r\n}\r\n\r\n"],"mappings":"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","names":["import_react","import_react","import_react"]}
@@ -26,4 +26,82 @@ declare function usePageProps<P = any, T = any>(): {
26
26
  props: P;
27
27
  };
28
28
 
29
- export { useBroadcastChannel, usePageProps };
29
+ interface Router {
30
+ /**
31
+ * Navigate to a new route.
32
+ * @param url - The URL to navigate to (e.g., "/about" or "/blog/[slug]" with params)
33
+ * @param options - Navigation options
34
+ */
35
+ push: (url: string, options?: {
36
+ revalidate?: boolean;
37
+ }) => Promise<void>;
38
+ /**
39
+ * Replace the current route without adding to history.
40
+ * @param url - The URL to navigate to
41
+ * @param options - Navigation options
42
+ */
43
+ replace: (url: string, options?: {
44
+ revalidate?: boolean;
45
+ }) => Promise<void>;
46
+ /**
47
+ * Go back in the browser history.
48
+ */
49
+ back: () => void;
50
+ /**
51
+ * Refresh the current route data by revalidating.
52
+ */
53
+ refresh: () => Promise<void>;
54
+ /**
55
+ * Current pathname (e.g., "/blog/my-post")
56
+ */
57
+ pathname: string;
58
+ /**
59
+ * Query parameters from the URL (e.g., ?id=123&name=test)
60
+ */
61
+ query: Record<string, string>;
62
+ /**
63
+ * Dynamic route parameters (e.g., { slug: "my-post" } for /blog/[slug])
64
+ */
65
+ params: Record<string, string>;
66
+ }
67
+ /**
68
+ * Hook to access router functionality and current route information.
69
+ *
70
+ * Provides methods to navigate programmatically and access current route data.
71
+ *
72
+ * @returns Router object with navigation methods and route information
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * function MyComponent() {
77
+ * const router = useRouter();
78
+ *
79
+ * const handleClick = () => {
80
+ * router.push("/about");
81
+ * };
82
+ *
83
+ * return (
84
+ * <div>
85
+ * <p>Current path: {router.pathname}</p>
86
+ * <p>Params: {JSON.stringify(router.params)}</p>
87
+ * <button onClick={handleClick}>Go to About</button>
88
+ * </div>
89
+ * );
90
+ * }
91
+ * ```
92
+ *
93
+ * @example
94
+ * ```tsx
95
+ * // Navigate with dynamic params
96
+ * router.push("/blog/my-post");
97
+ *
98
+ * // Replace current route
99
+ * router.replace("/login");
100
+ *
101
+ * // Refresh current route data
102
+ * await router.refresh();
103
+ * ```
104
+ */
105
+ declare function useRouter(): Router;
106
+
107
+ export { type Router, useBroadcastChannel, usePageProps, useRouter };
@@ -26,4 +26,82 @@ declare function usePageProps<P = any, T = any>(): {
26
26
  props: P;
27
27
  };
28
28
 
29
- export { useBroadcastChannel, usePageProps };
29
+ interface Router {
30
+ /**
31
+ * Navigate to a new route.
32
+ * @param url - The URL to navigate to (e.g., "/about" or "/blog/[slug]" with params)
33
+ * @param options - Navigation options
34
+ */
35
+ push: (url: string, options?: {
36
+ revalidate?: boolean;
37
+ }) => Promise<void>;
38
+ /**
39
+ * Replace the current route without adding to history.
40
+ * @param url - The URL to navigate to
41
+ * @param options - Navigation options
42
+ */
43
+ replace: (url: string, options?: {
44
+ revalidate?: boolean;
45
+ }) => Promise<void>;
46
+ /**
47
+ * Go back in the browser history.
48
+ */
49
+ back: () => void;
50
+ /**
51
+ * Refresh the current route data by revalidating.
52
+ */
53
+ refresh: () => Promise<void>;
54
+ /**
55
+ * Current pathname (e.g., "/blog/my-post")
56
+ */
57
+ pathname: string;
58
+ /**
59
+ * Query parameters from the URL (e.g., ?id=123&name=test)
60
+ */
61
+ query: Record<string, string>;
62
+ /**
63
+ * Dynamic route parameters (e.g., { slug: "my-post" } for /blog/[slug])
64
+ */
65
+ params: Record<string, string>;
66
+ }
67
+ /**
68
+ * Hook to access router functionality and current route information.
69
+ *
70
+ * Provides methods to navigate programmatically and access current route data.
71
+ *
72
+ * @returns Router object with navigation methods and route information
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * function MyComponent() {
77
+ * const router = useRouter();
78
+ *
79
+ * const handleClick = () => {
80
+ * router.push("/about");
81
+ * };
82
+ *
83
+ * return (
84
+ * <div>
85
+ * <p>Current path: {router.pathname}</p>
86
+ * <p>Params: {JSON.stringify(router.params)}</p>
87
+ * <button onClick={handleClick}>Go to About</button>
88
+ * </div>
89
+ * );
90
+ * }
91
+ * ```
92
+ *
93
+ * @example
94
+ * ```tsx
95
+ * // Navigate with dynamic params
96
+ * router.push("/blog/my-post");
97
+ *
98
+ * // Replace current route
99
+ * router.replace("/login");
100
+ *
101
+ * // Refresh current route data
102
+ * await router.refresh();
103
+ * ```
104
+ */
105
+ declare function useRouter(): Router;
106
+
107
+ export { type Router, useBroadcastChannel, usePageProps, useRouter };
@@ -51,8 +51,139 @@ function usePageProps() {
51
51
  }, []);
52
52
  return { params: state.params, props: state.props };
53
53
  }
54
+
55
+ // modules/react/hooks/useRouter/index.ts
56
+ import { useState as useState3, useEffect as useEffect3, useCallback, useContext as useContext2 } from "react";
57
+
58
+ // modules/runtime/client/RouterContext.tsx
59
+ import { createContext, useContext } from "react";
60
+ var RouterContext = createContext(null);
61
+
62
+ // modules/runtime/client/constants.ts
63
+ var WINDOW_DATA_KEY = "__FW_DATA__";
64
+
65
+ // modules/runtime/client/window-data.ts
66
+ function getWindowData() {
67
+ if (typeof window === "undefined") {
68
+ return null;
69
+ }
70
+ return window[WINDOW_DATA_KEY] ?? null;
71
+ }
72
+
73
+ // modules/react/hooks/useRouter/index.ts
74
+ function useRouter() {
75
+ const context = useContext2(RouterContext);
76
+ const navigate = context?.navigate;
77
+ const [routeData, setRouteData] = useState3(() => {
78
+ if (typeof window === "undefined") {
79
+ return {
80
+ pathname: "",
81
+ query: {},
82
+ params: {}
83
+ };
84
+ }
85
+ const data = getWindowData();
86
+ return {
87
+ pathname: data?.pathname || window.location.pathname,
88
+ query: parseQueryString(window.location.search),
89
+ params: data?.params || {}
90
+ };
91
+ });
92
+ useEffect3(() => {
93
+ if (typeof window === "undefined") return;
94
+ const handleDataRefresh = () => {
95
+ const data = getWindowData();
96
+ const currentPathname = window.location.pathname;
97
+ const currentSearch = window.location.search;
98
+ setRouteData({
99
+ pathname: data?.pathname || currentPathname,
100
+ query: parseQueryString(currentSearch),
101
+ params: data?.params || {}
102
+ });
103
+ };
104
+ window.addEventListener("fw-data-refresh", handleDataRefresh);
105
+ const handlePopState = () => {
106
+ handleDataRefresh();
107
+ };
108
+ window.addEventListener("popstate", handlePopState);
109
+ return () => {
110
+ window.removeEventListener("fw-data-refresh", handleDataRefresh);
111
+ window.removeEventListener("popstate", handlePopState);
112
+ };
113
+ }, []);
114
+ const push = useCallback(
115
+ async (url, options) => {
116
+ const fullUrl = url.startsWith("/") ? url : `/${url}`;
117
+ if (!navigate) {
118
+ if (typeof window !== "undefined") {
119
+ window.location.href = fullUrl;
120
+ }
121
+ return;
122
+ }
123
+ if (typeof window !== "undefined") {
124
+ window.history.pushState({}, "", fullUrl);
125
+ }
126
+ await navigate(fullUrl, options);
127
+ },
128
+ [navigate]
129
+ );
130
+ const replace = useCallback(
131
+ async (url, options) => {
132
+ const fullUrl = url.startsWith("/") ? url : `/${url}`;
133
+ if (!navigate) {
134
+ if (typeof window !== "undefined") {
135
+ window.location.replace(fullUrl);
136
+ }
137
+ return;
138
+ }
139
+ if (typeof window !== "undefined") {
140
+ window.history.replaceState({}, "", fullUrl);
141
+ }
142
+ await navigate(fullUrl, options);
143
+ },
144
+ [navigate]
145
+ );
146
+ const back = useCallback(() => {
147
+ if (typeof window !== "undefined") {
148
+ window.history.back();
149
+ }
150
+ }, []);
151
+ const refresh = useCallback(async () => {
152
+ const currentUrl = typeof window !== "undefined" ? window.location.pathname + window.location.search : routeData.pathname;
153
+ if (!navigate) {
154
+ if (typeof window !== "undefined") {
155
+ window.location.reload();
156
+ }
157
+ return;
158
+ }
159
+ await navigate(currentUrl, { revalidate: true });
160
+ }, [navigate, routeData.pathname]);
161
+ return {
162
+ push,
163
+ replace,
164
+ back,
165
+ refresh,
166
+ pathname: routeData.pathname,
167
+ query: routeData.query,
168
+ params: routeData.params
169
+ };
170
+ }
171
+ function parseQueryString(search) {
172
+ const params = {};
173
+ if (!search || search.length === 0) return params;
174
+ const queryString = search.startsWith("?") ? search.slice(1) : search;
175
+ const pairs = queryString.split("&");
176
+ for (const pair of pairs) {
177
+ const [key, value] = pair.split("=");
178
+ if (key) {
179
+ params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : "";
180
+ }
181
+ }
182
+ return params;
183
+ }
54
184
  export {
55
185
  useBroadcastChannel,
56
- usePageProps
186
+ usePageProps,
187
+ useRouter
57
188
  };
58
189
  //# sourceMappingURL=hooks.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/usePageProps/index.ts"],"sourcesContent":["import React, { useEffect, useState } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channel = new BroadcastChannel(channelName);\r\n\r\n useEffect(() => {\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n channel.close();\r\n };\r\n }, [channel]);\r\n\r\n const sendMessage = (msg: unknown) => {\r\n channel.postMessage(msg);\r\n };\r\n\r\n return { message, sendMessage };\r\n};\r\n","import React, { useEffect, useState } from \"react\";\r\n\r\n/**\r\n * Hook to access page props and route parameters.\r\n *\r\n * Reads initial data from window.__FW_DATA__ set during SSR.\r\n * Automatically updates when `revalidate()` is called.\r\n *\r\n * @template P - Type for page props (default: any)\r\n * @template T - Type for route params (default: any)\r\n * @returns Object containing params and props\r\n *\r\n * @example\r\n * // With props type only\r\n * const { props } = usePageProps<{ title: string }>();\r\n *\r\n * @example\r\n * // With both props and params types\r\n * const { props, params } = usePageProps<{ title: string }, { id: string }>();\r\n */\r\nexport function usePageProps<P = any, T = any>(): { params: T, props: P } {\r\n const [state, setState] = useState<{ params: T, props: P }>(() => {\r\n // Initialize with current data if available\r\n if (typeof window !== \"undefined\" && (window as any)?.__FW_DATA__) {\r\n const data = (window as any).__FW_DATA__;\r\n return {\r\n params: data.params || {} as T,\r\n props: data.props || {} as P,\r\n };\r\n }\r\n return {\r\n params: {} as T,\r\n props: {} as P,\r\n };\r\n });\r\n\r\n useEffect(() => {\r\n // Listen for data refresh events (from revalidate() or navigation)\r\n const handleDataRefresh = () => {\r\n if ((window as any)?.__FW_DATA__) {\r\n const data = (window as any).__FW_DATA__;\r\n setState({\r\n params: data.params || {} as T,\r\n props: data.props || {} as P,\r\n });\r\n }\r\n };\r\n\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n };\r\n }, []);\r\n\r\n return { params: state.params as T, props: state.props as P };\r\n};\r\n"],"mappings":";AAAA,SAAgB,WAAW,gBAAgB;AAEpC,IAAM,sBAAsB,CAAC,gBAAwB;AAC1D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,QAAM,UAAU,IAAI,iBAAiB,WAAW;AAEhD,YAAU,MAAM;AACd,UAAM,gBAAgB,CAAC,UAAwB;AAC7C,iBAAW,MAAM,IAAI;AAAA,IACvB;AAEA,YAAQ,YAAY;AAGpB,WAAO,MAAM;AACX,cAAQ,MAAM;AAAA,IAChB;AAAA,EACF,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,cAAc,CAAC,QAAiB;AACpC,YAAQ,YAAY,GAAG;AAAA,EACzB;AAEA,SAAO,EAAE,SAAS,YAAY;AAChC;;;ACxBA,SAAgB,aAAAA,YAAW,YAAAC,iBAAgB;AAoBpC,SAAS,eAA0D;AACxE,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAkC,MAAM;AAEhE,QAAI,OAAO,WAAW,eAAgB,QAAgB,aAAa;AACjE,YAAM,OAAQ,OAAe;AAC7B,aAAO;AAAA,QACL,QAAQ,KAAK,UAAU,CAAC;AAAA,QACxB,OAAO,KAAK,SAAS,CAAC;AAAA,MACxB;AAAA,IACF;AACA,WAAO;AAAA,MACL,QAAQ,CAAC;AAAA,MACT,OAAO,CAAC;AAAA,IACV;AAAA,EACF,CAAC;AAED,EAAAD,WAAU,MAAM;AAEd,UAAM,oBAAoB,MAAM;AAC9B,UAAK,QAAgB,aAAa;AAChC,cAAM,OAAQ,OAAe;AAC7B,iBAAS;AAAA,UACP,QAAQ,KAAK,UAAU,CAAC;AAAA,UACxB,OAAO,KAAK,SAAS,CAAC;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAEA,WAAO,iBAAiB,mBAAmB,iBAAiB;AAE5D,WAAO,MAAM;AACX,aAAO,oBAAoB,mBAAmB,iBAAiB;AAAA,IACjE;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,QAAQ,MAAM,QAAa,OAAO,MAAM,MAAW;AAC9D;","names":["useEffect","useState"]}
1
+ {"version":3,"sources":["../../modules/react/hooks/useBroadcastChannel/index.tsx","../../modules/react/hooks/usePageProps/index.ts","../../modules/react/hooks/useRouter/index.ts","../../modules/runtime/client/RouterContext.tsx","../../modules/runtime/client/constants.ts","../../modules/runtime/client/window-data.ts"],"sourcesContent":["import React, { useEffect, useState } from \"react\";\r\n\r\nexport const useBroadcastChannel = (channelName: string) => {\r\n const [message, setMessage] = useState(null);\r\n const channel = new BroadcastChannel(channelName);\r\n\r\n useEffect(() => {\r\n const handleMessage = (event: MessageEvent) => {\r\n setMessage(event.data);\r\n };\r\n\r\n channel.onmessage = handleMessage;\r\n\r\n // Clean up the channel when the component unmounts\r\n return () => {\r\n channel.close();\r\n };\r\n }, [channel]);\r\n\r\n const sendMessage = (msg: unknown) => {\r\n channel.postMessage(msg);\r\n };\r\n\r\n return { message, sendMessage };\r\n};\r\n","import React, { useEffect, useState } from \"react\";\r\n\r\n/**\r\n * Hook to access page props and route parameters.\r\n *\r\n * Reads initial data from window.__FW_DATA__ set during SSR.\r\n * Automatically updates when `revalidate()` is called.\r\n *\r\n * @template P - Type for page props (default: any)\r\n * @template T - Type for route params (default: any)\r\n * @returns Object containing params and props\r\n *\r\n * @example\r\n * // With props type only\r\n * const { props } = usePageProps<{ title: string }>();\r\n *\r\n * @example\r\n * // With both props and params types\r\n * const { props, params } = usePageProps<{ title: string }, { id: string }>();\r\n */\r\nexport function usePageProps<P = any, T = any>(): { params: T, props: P } {\r\n const [state, setState] = useState<{ params: T, props: P }>(() => {\r\n // Initialize with current data if available\r\n if (typeof window !== \"undefined\" && (window as any)?.__FW_DATA__) {\r\n const data = (window as any).__FW_DATA__;\r\n return {\r\n params: data.params || {} as T,\r\n props: data.props || {} as P,\r\n };\r\n }\r\n return {\r\n params: {} as T,\r\n props: {} as P,\r\n };\r\n });\r\n\r\n useEffect(() => {\r\n // Listen for data refresh events (from revalidate() or navigation)\r\n const handleDataRefresh = () => {\r\n if ((window as any)?.__FW_DATA__) {\r\n const data = (window as any).__FW_DATA__;\r\n setState({\r\n params: data.params || {} as T,\r\n props: data.props || {} as P,\r\n });\r\n }\r\n };\r\n\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n };\r\n }, []);\r\n\r\n return { params: state.params as T, props: state.props as P };\r\n};\r\n","import { useState, useEffect, useCallback, useContext } from \"react\";\r\nimport { RouterContext } from \"../../../runtime/client/RouterContext\";\r\nimport { getWindowData } from \"../../../runtime/client/window-data\";\r\n\r\nexport interface Router {\r\n /**\r\n * Navigate to a new route.\r\n * @param url - The URL to navigate to (e.g., \"/about\" or \"/blog/[slug]\" with params)\r\n * @param options - Navigation options\r\n */\r\n push: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Replace the current route without adding to history.\r\n * @param url - The URL to navigate to\r\n * @param options - Navigation options\r\n */\r\n replace: (url: string, options?: { revalidate?: boolean }) => Promise<void>;\r\n \r\n /**\r\n * Go back in the browser history.\r\n */\r\n back: () => void;\r\n \r\n /**\r\n * Refresh the current route data by revalidating.\r\n */\r\n refresh: () => Promise<void>;\r\n \r\n /**\r\n * Current pathname (e.g., \"/blog/my-post\")\r\n */\r\n pathname: string;\r\n \r\n /**\r\n * Query parameters from the URL (e.g., ?id=123&name=test)\r\n */\r\n query: Record<string, string>;\r\n \r\n /**\r\n * Dynamic route parameters (e.g., { slug: \"my-post\" } for /blog/[slug])\r\n */\r\n params: Record<string, string>;\r\n}\r\n\r\n/**\r\n * Hook to access router functionality and current route information.\r\n * \r\n * Provides methods to navigate programmatically and access current route data.\r\n * \r\n * @returns Router object with navigation methods and route information\r\n * \r\n * @example\r\n * ```tsx\r\n * function MyComponent() {\r\n * const router = useRouter();\r\n * \r\n * const handleClick = () => {\r\n * router.push(\"/about\");\r\n * };\r\n * \r\n * return (\r\n * <div>\r\n * <p>Current path: {router.pathname}</p>\r\n * <p>Params: {JSON.stringify(router.params)}</p>\r\n * <button onClick={handleClick}>Go to About</button>\r\n * </div>\r\n * );\r\n * }\r\n * ```\r\n * \r\n * @example\r\n * ```tsx\r\n * // Navigate with dynamic params\r\n * router.push(\"/blog/my-post\");\r\n * \r\n * // Replace current route\r\n * router.replace(\"/login\");\r\n * \r\n * // Refresh current route data\r\n * await router.refresh();\r\n * ```\r\n */\r\nexport function useRouter(): Router {\r\n // Try to get context, but don't throw if it's not available (SSR)\r\n const context = useContext(RouterContext);\r\n const navigate = context?.navigate;\r\n \r\n const [routeData, setRouteData] = useState(() => {\r\n // During SSR, return empty/default values\r\n if (typeof window === \"undefined\") {\r\n return {\r\n pathname: \"\",\r\n query: {},\r\n params: {},\r\n };\r\n }\r\n \r\n // On client, get data from window\r\n const data = getWindowData();\r\n return {\r\n pathname: data?.pathname || window.location.pathname,\r\n query: parseQueryString(window.location.search),\r\n params: data?.params || {},\r\n };\r\n });\r\n\r\n // Listen for route changes (only on client)\r\n useEffect(() => {\r\n if (typeof window === \"undefined\") return;\r\n \r\n const handleDataRefresh = () => {\r\n const data = getWindowData();\r\n const currentPathname = window.location.pathname;\r\n const currentSearch = window.location.search;\r\n \r\n setRouteData({\r\n pathname: data?.pathname || currentPathname,\r\n query: parseQueryString(currentSearch),\r\n params: data?.params || {},\r\n });\r\n };\r\n\r\n // Listen for navigation events\r\n window.addEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n \r\n // Also listen for popstate (browser back/forward)\r\n const handlePopState = () => {\r\n handleDataRefresh();\r\n };\r\n window.addEventListener(\"popstate\", handlePopState);\r\n\r\n return () => {\r\n window.removeEventListener(\"fw-data-refresh\", handleDataRefresh);\r\n window.removeEventListener(\"popstate\", handlePopState);\r\n };\r\n }, []);\r\n\r\n const push = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n // During SSR or if context is not available, use window.location\r\n if (!navigate) {\r\n if (typeof window !== \"undefined\") {\r\n window.location.href = fullUrl;\r\n }\r\n return;\r\n }\r\n \r\n if (typeof window !== \"undefined\") {\r\n window.history.pushState({}, \"\", fullUrl);\r\n }\r\n await navigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const replace = useCallback(\r\n async (url: string, options?: { revalidate?: boolean }) => {\r\n const fullUrl = url.startsWith(\"/\") ? url : `/${url}`;\r\n \r\n // During SSR or if context is not available, use window.location\r\n if (!navigate) {\r\n if (typeof window !== \"undefined\") {\r\n window.location.replace(fullUrl);\r\n }\r\n return;\r\n }\r\n \r\n if (typeof window !== \"undefined\") {\r\n window.history.replaceState({}, \"\", fullUrl);\r\n }\r\n await navigate(fullUrl, options);\r\n },\r\n [navigate]\r\n );\r\n\r\n const back = useCallback(() => {\r\n if (typeof window !== \"undefined\") {\r\n window.history.back();\r\n }\r\n }, []);\r\n\r\n const refresh = useCallback(async () => {\r\n const currentUrl = typeof window !== \"undefined\" \r\n ? window.location.pathname + window.location.search \r\n : routeData.pathname;\r\n \r\n // During SSR or if context is not available, reload the page\r\n if (!navigate) {\r\n if (typeof window !== \"undefined\") {\r\n window.location.reload();\r\n }\r\n return;\r\n }\r\n \r\n await navigate(currentUrl, { revalidate: true });\r\n }, [navigate, routeData.pathname]);\r\n\r\n return {\r\n push,\r\n replace,\r\n back,\r\n refresh,\r\n pathname: routeData.pathname,\r\n query: routeData.query,\r\n params: routeData.params,\r\n };\r\n}\r\n\r\n/**\r\n * Parse query string into an object.\r\n * @param search - Query string (e.g., \"?id=123&name=test\")\r\n * @returns Object with query parameters\r\n */\r\nfunction parseQueryString(search: string): Record<string, string> {\r\n const params: Record<string, string> = {};\r\n if (!search || search.length === 0) return params;\r\n \r\n const queryString = search.startsWith(\"?\") ? search.slice(1) : search;\r\n const pairs = queryString.split(\"&\");\r\n \r\n for (const pair of pairs) {\r\n const [key, value] = pair.split(\"=\");\r\n if (key) {\r\n params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : \"\";\r\n }\r\n }\r\n \r\n return params;\r\n}\r\n","import { createContext, useContext } from \"react\";\r\n\r\nexport type NavigateFunction = (\r\n url: string,\r\n options?: { revalidate?: boolean; replace?: boolean }\r\n) => Promise<void>;\r\n\r\nexport interface RouterContextValue {\r\n navigate: NavigateFunction;\r\n}\r\n\r\nexport const RouterContext = createContext<RouterContextValue | null>(null);\r\n\r\nexport function useRouterContext(): RouterContextValue {\r\n const context = useContext(RouterContext);\r\n if (!context) {\r\n throw new Error(\r\n \"useRouter must be used within a RouterProvider. Make sure you're using it inside a Loly app.\"\r\n );\r\n }\r\n return context;\r\n}\r\n","// Client-side constants (hardcoded to avoid alias resolution issues in Rspack)\r\nexport const WINDOW_DATA_KEY = \"__FW_DATA__\";\r\nexport const APP_CONTAINER_ID = \"__app\";\r\n\r\n","import { WINDOW_DATA_KEY } from \"./constants\";\r\nimport type { InitialData } from \"./types\";\r\n\r\nexport function getWindowData(): InitialData | null {\r\n if (typeof window === \"undefined\") {\r\n return null;\r\n }\r\n return ((window as any)[WINDOW_DATA_KEY] as InitialData | undefined) ?? null;\r\n}\r\n\r\nexport function setWindowData(data: InitialData): void {\r\n (window as any)[WINDOW_DATA_KEY] = data;\r\n \r\n // Dispatch event for components to listen to (e.g., usePageProps, ThemeProvider)\r\n // This ensures components update when navigating in SPA mode\r\n if (typeof window !== \"undefined\") {\r\n window.dispatchEvent(\r\n new CustomEvent(\"fw-data-refresh\", {\r\n detail: { data },\r\n })\r\n );\r\n }\r\n}\r\n\r\nexport function getCurrentTheme(): string | null {\r\n return getWindowData()?.theme ?? null;\r\n}\r\n\r\n"],"mappings":"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","names":["useEffect","useState","useState","useEffect","useContext","useContext","useState","useEffect"]}
package/dist/runtime.cjs CHANGED
@@ -33,6 +33,9 @@ var APP_CONTAINER_ID = "__app";
33
33
 
34
34
  // modules/runtime/client/window-data.ts
35
35
  function getWindowData() {
36
+ if (typeof window === "undefined") {
37
+ return null;
38
+ }
36
39
  return window[WINDOW_DATA_KEY] ?? null;
37
40
  }
38
41
  function setWindowData(data) {
@@ -109,7 +112,7 @@ function applyMetadata(md) {
109
112
  }
110
113
 
111
114
  // modules/runtime/client/AppShell.tsx
112
- var import_react = require("react");
115
+ var import_react2 = require("react");
113
116
 
114
117
  // modules/runtime/client/RouterView.tsx
115
118
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -528,6 +531,10 @@ function createPopStateHandler(navigate2) {
528
531
  };
529
532
  }
530
533
 
534
+ // modules/runtime/client/RouterContext.tsx
535
+ var import_react = require("react");
536
+ var RouterContext = (0, import_react.createContext)(null);
537
+
531
538
  // modules/runtime/client/AppShell.tsx
532
539
  var import_jsx_runtime2 = require("react/jsx-runtime");
533
540
  function AppShell({
@@ -536,14 +543,14 @@ function AppShell({
536
543
  notFoundRoute,
537
544
  errorRoute
538
545
  }) {
539
- const [state, setState] = (0, import_react.useState)(initialState);
540
- const handlersRef = (0, import_react.useRef)({
546
+ const [state, setState] = (0, import_react2.useState)(initialState);
547
+ const handlersRef = (0, import_react2.useRef)({
541
548
  setState,
542
549
  routes,
543
550
  notFoundRoute,
544
551
  errorRoute
545
552
  });
546
- (0, import_react.useEffect)(() => {
553
+ (0, import_react2.useEffect)(() => {
547
554
  handlersRef.current = {
548
555
  setState,
549
556
  routes,
@@ -551,14 +558,22 @@ function AppShell({
551
558
  errorRoute
552
559
  };
553
560
  }, [routes, notFoundRoute, errorRoute]);
554
- (0, import_react.useEffect)(() => {
561
+ const handleNavigate = (0, import_react2.useCallback)(
562
+ async (nextUrl, options) => {
563
+ await navigate(nextUrl, handlersRef.current, {
564
+ revalidate: options?.revalidate
565
+ });
566
+ },
567
+ []
568
+ );
569
+ (0, import_react2.useEffect)(() => {
555
570
  let isMounted = true;
556
- async function handleNavigate(nextUrl, options) {
571
+ async function handleNavigateInternal(nextUrl, options) {
557
572
  if (!isMounted) return;
558
573
  await navigate(nextUrl, handlersRef.current, options);
559
574
  }
560
- const handleClick = createClickHandler(handleNavigate);
561
- const handlePopState = createPopStateHandler(handleNavigate);
575
+ const handleClick = createClickHandler(handleNavigateInternal);
576
+ const handlePopState = createPopStateHandler(handleNavigateInternal);
562
577
  window.addEventListener("click", handleClick, false);
563
578
  window.addEventListener("popstate", handlePopState, false);
564
579
  return () => {
@@ -571,7 +586,7 @@ function AppShell({
571
586
  const isNotFound = state.route === notFoundRoute;
572
587
  const routeType = isError ? "error" : isNotFound ? "notfound" : "normal";
573
588
  const routeKey = `${state.url}:${routeType}`;
574
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterView, { state }, routeKey);
589
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterContext.Provider, { value: { navigate: handleNavigate }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(RouterView, { state }, routeKey) });
575
590
  }
576
591
 
577
592
  // modules/runtime/client/bootstrap.tsx