@funstack/router 0.0.1 → 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 uhyo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.d.mts CHANGED
@@ -23,6 +23,32 @@ type LoaderArgs = {
23
23
  /** AbortSignal for cancellation on navigation */
24
24
  signal: AbortSignal;
25
25
  };
26
+ /**
27
+ * Props for route components without loader.
28
+ * Includes navigation state management props.
29
+ */
30
+ type RouteComponentProps<TParams extends Record<string, string>, TState = undefined> = {
31
+ /** Extracted path parameters */
32
+ params: TParams;
33
+ /** Current navigation state for this route (undefined on first visit) */
34
+ state: TState | undefined;
35
+ /** Update navigation state for this route asynchronously via replace navigation */
36
+ setState: (state: TState | ((prev: TState | undefined) => TState)) => Promise<void>;
37
+ /** Update navigation state for this route synchronously via updateCurrentEntry */
38
+ setStateSync: (state: TState | ((prev: TState | undefined) => TState)) => void;
39
+ /** Reset navigation state to undefined */
40
+ resetState: () => void;
41
+ /** Ephemeral navigation info (only available during navigation, not persisted) */
42
+ info: unknown;
43
+ };
44
+ /**
45
+ * Props for route components with loader.
46
+ * Includes data from loader and navigation state management props.
47
+ */
48
+ type RouteComponentPropsWithData<TParams extends Record<string, string>, TData, TState = undefined> = RouteComponentProps<TParams, TState> & {
49
+ /** Data returned from the loader */
50
+ data: TData;
51
+ };
26
52
  /**
27
53
  * Route definition created by the `route` helper function.
28
54
  */
@@ -36,31 +62,28 @@ interface OpaqueRouteDefinition {
36
62
  */
37
63
  type RouteDefinition = OpaqueRouteDefinition | {
38
64
  path: string;
39
- component?: ComponentType<{}>;
65
+ component?: ComponentType<object>;
40
66
  children?: RouteDefinition[];
41
67
  };
42
68
  /**
43
69
  * Route definition with loader - infers TData from loader return type.
44
70
  * TPath is used to infer params type from the path pattern.
71
+ * TState is the type of navigation state for this route.
45
72
  */
46
- type RouteWithLoader<TPath extends string, TData> = {
73
+ type RouteWithLoader<TPath extends string, TData, TState> = {
47
74
  path: TPath;
48
75
  loader: (args: LoaderArgs) => TData;
49
- component: ComponentType<{
50
- data: TData;
51
- params: PathParams<TPath>;
52
- }>;
76
+ component: ComponentType<RouteComponentPropsWithData<PathParams<TPath>, TData, TState>>;
53
77
  children?: RouteDefinition[];
54
78
  };
55
79
  /**
56
80
  * Route definition without loader.
57
81
  * TPath is used to infer params type from the path pattern.
82
+ * TState is the type of navigation state for this route.
58
83
  */
59
- type RouteWithoutLoader<TPath extends string> = {
84
+ type RouteWithoutLoader<TPath extends string, TState> = {
60
85
  path: TPath;
61
- component?: ComponentType<{
62
- params: PathParams<TPath>;
63
- }>;
86
+ component?: ComponentType<RouteComponentProps<PathParams<TPath>, TState>>;
64
87
  children?: RouteDefinition[];
65
88
  };
66
89
  /**
@@ -70,6 +93,8 @@ type RouteWithoutLoader<TPath extends string> = {
70
93
  * the component accepts a `data` prop of that type. Components always receive
71
94
  * a `params` prop with types inferred from the path pattern.
72
95
  *
96
+ * For routes with navigation state, use `routeState<TState>()({ ... })` instead.
97
+ *
73
98
  * @example
74
99
  * ```typescript
75
100
  * // Route with async loader
@@ -79,21 +104,54 @@ type RouteWithoutLoader<TPath extends string> = {
79
104
  * const res = await fetch(`/api/users/${params.userId}`, { signal });
80
105
  * return res.json() as Promise<User>;
81
106
  * },
82
- * component: UserDetail, // Must accept { data: Promise<User>, params: { userId: string } }
107
+ * component: UserDetail, // Must accept { data: Promise<User>, params: { userId: string }, state, setState, resetState }
83
108
  * });
84
109
  *
85
110
  * // Route without loader
86
111
  * route({
87
112
  * path: "about",
88
- * component: AboutPage, // Must accept { params: {} }
113
+ * component: AboutPage, // Must accept { params: {}, state, setState, resetState }
114
+ * });
115
+ * ```
116
+ */
117
+ declare function route<TPath extends string, TData>(definition: RouteWithLoader<TPath, TData, undefined>): OpaqueRouteDefinition;
118
+ declare function route<TPath extends string>(definition: RouteWithoutLoader<TPath, undefined>): OpaqueRouteDefinition;
119
+ /**
120
+ * Helper function for creating type-safe route definitions with navigation state.
121
+ *
122
+ * Use this curried function when your route component needs to manage navigation state.
123
+ * The state is tied to the navigation history entry and persists across back/forward navigation.
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * // Route with navigation state
128
+ * type MyState = { scrollPosition: number };
129
+ * routeState<MyState>()({
130
+ * path: "users/:userId",
131
+ * component: UserPage, // Receives { params, state, setState, resetState }
132
+ * });
133
+ *
134
+ * // Route with both loader and navigation state
135
+ * type FilterState = { filter: string };
136
+ * routeState<FilterState>()({
137
+ * path: "products",
138
+ * loader: async () => fetchProducts(),
139
+ * component: ProductList, // Receives { data, params, state, setState, resetState }
89
140
  * });
90
141
  * ```
91
142
  */
92
- declare function route<TPath extends string, TData>(definition: RouteWithLoader<TPath, TData>): OpaqueRouteDefinition;
93
- declare function route<TPath extends string>(definition: RouteWithoutLoader<TPath>): OpaqueRouteDefinition;
143
+ declare function routeState<TState>(): {
144
+ <TPath extends string, TData>(definition: RouteWithLoader<TPath, TData, TState>): OpaqueRouteDefinition;
145
+ <TPath extends string>(definition: RouteWithoutLoader<TPath, TState>): OpaqueRouteDefinition;
146
+ };
94
147
  //#endregion
95
148
  //#region src/types.d.ts
96
149
  declare const InternalRouteDefinitionSymbol: unique symbol;
150
+ /**
151
+ * Internal structure for storing per-route state in NavigationHistoryEntry.
152
+ * Each route in the matched stack gets its own state slot indexed by match position.
153
+ */
154
+
97
155
  /**
98
156
  * Route definition for the router.
99
157
  * When a loader is defined, the component receives the loader result as a `data` prop.
@@ -110,6 +168,11 @@ type InternalRouteDefinition = {
110
168
  component?: ComponentType<{
111
169
  data?: unknown;
112
170
  params?: Record<string, string>;
171
+ state?: unknown;
172
+ setState?: (state: unknown | ((prev: unknown) => unknown)) => Promise<void>;
173
+ setStateSync?: (state: unknown | ((prev: unknown) => unknown)) => void;
174
+ resetState?: () => void;
175
+ info?: unknown;
113
176
  }>;
114
177
  };
115
178
  /**
@@ -138,6 +201,8 @@ type NavigateOptions = {
138
201
  replace?: boolean;
139
202
  /** State to associate with the navigation */
140
203
  state?: unknown;
204
+ /** Ephemeral info for this navigation only (not persisted in history) */
205
+ info?: unknown;
141
206
  };
142
207
  /**
143
208
  * Location object representing current URL state.
@@ -207,12 +272,6 @@ declare function useNavigate(): (to: string, options?: NavigateOptions) => void;
207
272
  */
208
273
  declare function useLocation(): Location;
209
274
  //#endregion
210
- //#region src/hooks/useParams.d.ts
211
- /**
212
- * Returns route parameters from the matched path.
213
- */
214
- declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
215
- //#endregion
216
275
  //#region src/hooks/useSearchParams.d.ts
217
276
  type SetSearchParams = (params: URLSearchParams | Record<string, string> | ((prev: URLSearchParams) => URLSearchParams | Record<string, string>)) => void;
218
277
  /**
@@ -220,6 +279,43 @@ type SetSearchParams = (params: URLSearchParams | Record<string, string> | ((pre
220
279
  */
221
280
  declare function useSearchParams(): [URLSearchParams, SetSearchParams];
222
281
  //#endregion
282
+ //#region src/hooks/useBlocker.d.ts
283
+ type UseBlockerOptions = {
284
+ /**
285
+ * Function that returns true if navigation should be blocked.
286
+ * Can call `confirm()` inside to show a confirmation dialog.
287
+ */
288
+ shouldBlock: () => boolean;
289
+ };
290
+ /**
291
+ * Hook to block navigation away from the current route.
292
+ *
293
+ * This is useful for scenarios like unsaved form data, ongoing file uploads,
294
+ * or any state that would be lost on navigation.
295
+ *
296
+ * @example
297
+ * ```tsx
298
+ * function EditForm() {
299
+ * const [isDirty, setIsDirty] = useState(false);
300
+ *
301
+ * useBlocker({
302
+ * shouldBlock: () => {
303
+ * if (isDirty) {
304
+ * return !confirm("You have unsaved changes. Leave anyway?");
305
+ * }
306
+ * return false;
307
+ * },
308
+ * });
309
+ *
310
+ * return <form>...</form>;
311
+ * }
312
+ * ```
313
+ *
314
+ * Note: This hook only handles SPA navigations (links, programmatic navigation).
315
+ * For hard navigations (tab close, refresh), handle `beforeunload` separately.
316
+ */
317
+ declare function useBlocker(options: UseBlockerOptions): void;
318
+ //#endregion
223
319
  //#region src/core/RouterAdapter.d.ts
224
320
  /**
225
321
  * Represents the current location state.
@@ -232,7 +328,9 @@ type LocationEntry = {
232
328
  key: string;
233
329
  /** State associated with this entry */
234
330
  state: unknown;
331
+ /** Ephemeral info from current navigation (undefined if not from navigation event) */
332
+ info: unknown;
235
333
  };
236
334
  //#endregion
237
- export { type FallbackMode, type LoaderArgs, type Location, type LocationEntry, type MatchedRoute, type MatchedRouteWithData, type NavigateOptions, type OnNavigateCallback, Outlet, type PathParams, type RouteDefinition, Router, type RouterProps, route, useLocation, useNavigate, useParams, useSearchParams };
335
+ export { type FallbackMode, type LoaderArgs, type Location, type LocationEntry, type MatchedRoute, type MatchedRouteWithData, type NavigateOptions, type OnNavigateCallback, Outlet, type PathParams, type RouteComponentProps, type RouteComponentPropsWithData, type RouteDefinition, Router, type RouterProps, type UseBlockerOptions, route, routeState, useBlocker, useLocation, useNavigate, useSearchParams };
238
336
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/route.ts","../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useParams.ts","../src/hooks/useSearchParams.ts","../src/core/RouterAdapter.ts"],"sourcesContent":[],"mappings":";;;cAEM;;AAFqC;AAEL;;KAMjC,aAEC,CAAA,UAAA,MAAA,CAAA,GADJ,CACI,SAAA,GAAA,MAAA,IAAA,KAAA,MAAA,IAAA,KAAA,KAAA,EAAA,GAAA,KAAA,GAAQ,aAAR,CAAA,IAA0B,IAA1B,EAAA,CAAA,GACA,CADA,SAAA,GAAA,MAAA,IAAA,KAAA,MAAA,EAAA,GAAA,KAAA,GAAA,KAAA;;;;;AASM,KAAA,UAAU,CAAA,UAAA,MAAA,CAAA,GAAA,CAAsB,aAAtB,CAAoC,CAApC,CAAA,CAAA,SAAA,CAAA,KAAA,CAAA,GAClB,MADkB,CAAA,MAAA,EAAA,KAAA,CAAA,GAAA,QAEV,aAF8C,CAEhC,CAFgC,CAAA,GAAA,MAAA,EAAd;;;;AAEnB,KAKb,UAAA,GALa;EAKb;EAEF,MAAA,EAAA,MAAA,CAAA,MAAA,EAAA,MAAA,CAAA;EAEC;EAED,OAAA,EAFC,OAED;EAAW;EAMJ,MAAA,EANP,WAMO;AASjB,CAAA;;;;AAKgC,UAdf,qBAAA,CAce;EAO3B,CApBF,qBAAA,CAoBiB,EAAA,KAAA;EACZ,IAAA,EAAA,MAAA;EACS,QAAA,CAAA,EApBJ,eAoBI,EAAA;;;;;AACJ,KAfD,eAAA,GACR,qBAcS,GAAA;EAEA,IAAA,EAAA,MAAA;EAAe,SAAA,CAAA,EAbV,aAaU,CAAA,CAAA,CAAA,CAAA;EAOvB,QAAA,CAAA,EAnBY,eAmBM,EAAA;CACf;;;;;KAbH,eAgBuB,CAAA,cAAA,MAAA,EAAA,KAAA,CAAA,GAAA;EA6BZ,IAAA,EA5CR,KA4Ca;EACS,MAAA,EAAA,CAAA,IAAA,EA5Cb,UA4Ca,EAAA,GA5CE,KA4CF;EAAO,SAAA,EA3CxB,aA2CwB,CAAA;IAAvB,IAAA,EA3CqB,KA2CrB;IACX,MAAA,EA5C+C,UA4C/C,CA5C0D,KA4C1D,CAAA;EAAqB,CAAA,CAAA;EACR,QAAK,CAAA,EA3CR,eA2CQ,EAAA;CACY;;;;;KArC5B;QACG;ECrEF,SAAA,CAAA,EDsEQ,aCtER,CAAA;IAMM,MAAA,EDgE0B,UChE1B,CDgEqC,KChEd,CAAA;EAChC,CAAA,CAAA;EAIU,QAAA,CAAA,ED6DA,eC7DA,EAAA;CAMK;;;;AAwBlB;AAYA;AAQA;AAUA;AAaA;AAWA;;;;AC7EA;;;;;AAmBA;;;;;;;;;iBFgEgB,+CACF,gBAAgB,OAAO,SAClC;AGnGa,iBHoGA,KGpGU,CAAA,cAAS,MAAA,CAAA,CAAA,UAAA,EHqGrB,kBGrGqB,CHqGF,KGrGE,CAAA,CAAA,EHsGhC,qBGtGgC;;;cFJ7B;ADHqC;AAEL;;;AAQN,KCDpB,uBAAA,GDCoB;EAAlB,CCAX,6BAAA,CDAW,EAAA,KAAA;EACR;EAAC,IAAA,EAAA,MAAA;EAQK;EAA8C,QAAA,CAAA,ECL7C,uBDK6C,EAAA;EAAd;EACxC,MAAA,CAAA,EAAA,CAAA,IAAA,ECAc,UDAd,EAAA,GAAA,OAAA;EACsB;EAAd,SAAA,CAAA,ECCE,aDDF,CAAA;IAAa,IAAA,CAAA,EAAA,OAAA;IAKb,MAAA,CAAA,ECFC,MDES,CAAA,MAAA,EAAA,MAAA,CAAA;EAEZ,CAAA,CAAA;CAEC;AAuBL;;;AAQ0B,KCjBpB,YAAA,GDiBoB;EACG;EAA0B,KAAA,EChBpD,uBDgBoD;EAAX;EAArC,MAAA,ECdH,MDcG,CAAA,MAAA,EAAA,MAAA,CAAA;EAEA;EAAe,QAAA,EAAA,MAAA;AAAA,CAAA;;;;AASd,KCjBF,oBAAA,GAAuB,YDiBrB,GAAA;EAED;EAAe,IAAA,EAAA,OAAA,GAAA,SAAA;AA6B5B,CAAA;;;;AAEG,KC1CS,eAAA,GD0CT;EAAqB;EACR,OAAA,CAAK,EAAA,OAAA;EACY;EAAnB,KAAA,CAAA,EAAA,OAAA;CACX;;;;KCnCS,QAAA;EAvEN,QAAA,EAAA,MAAA;EAMM,MAAA,EAAA,MAAA;EACT,IAAA,EAAA,MAAA;CAIU;;;;;AA8Bb;AAYA;AAQA;AAUY,KAaA,kBAAA,GAbQ,CAAA,KAAA,EAcX,aAdW,EAAA,OAAA,EAAA,SAeA,YAfA,EAAA,GAAA,IAAA,EAAA,GAAA,IAAA;AAapB;AAWA;;;;AC7EA;AACU,KD4EE,YAAA,GC5EF,MAAA,GAAA,QAAA;;;AFpBJ,KEmBM,WAAA,GFnBN;EAMD,MAAA,EEcK,eFdQ,EAAA;EAChB;;;;;;AAUF;EAA0D,UAAA,CAAA,EEW3C,kBFX2C;EAAd;;;;;AAO5C;EAEU,QAAA,CAAA,EESG,YFTH;CAEC;AAED,iBEQM,MAAA,CFRN;EAAA,MAAA,EESA,WFTA;EAAA,UAAA;EAAA;AAAA,CAAA,EEYP,WFZO,CAAA,EEYO,SFZP;;;;;AAhCiC;AAEL;AAOpC,iBGFc,MAAA,CAAA,CHEd,EGFwB,SHExB;;;;;AATyC;AAQtC,iBIDW,WAAA,CAAA,CJCE,EAAA,CAAA,EAAA,EAAA,MAAA,EAAA,OAAA,CAAA,EIDoC,eJCpC,EAAA,GAAA,IAAA;;;;;AARyB;AAQtC,iBKDW,WAAA,CAAA,CLCE,EKDa,QLCb;;;;;;AANZ,iBMIU,SNJsB,CAAA,UMK1B,MNL0B,CAAA,MAAA,EAAA,MAAA,CAAA,GMKD,MNLC,CAAA,MAAA,EAAA,MAAA,CAAA,CAAA,CAAA,CAAA,EMMjC,CNNiC;;;KOCjC,eAAA,YAEC,kBACA,iCACQ,oBAAoB,kBAAkB;;;APPT;AAQtC,iBOKW,eAAA,CAAA,CPLE,EAAA,COKkB,ePLlB,EOKmC,ePLnC,CAAA;;;;;AARyB;AAEL;AAOpC,KQCU,aAAA,GRDV;EACI;EAA0B,GAAA,EQEzB,GRFyB;EAAlB;EACR,GAAA,EAAA,MAAA;EAAC;EAQK,KAAA,EAAA,OAAU;CAAoC"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/route.ts","../src/types.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/core/RouterAdapter.ts"],"sourcesContent":[],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useCallback, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
1
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useState, useSyncExternalStore } from "react";
2
2
  import { jsx } from "react/jsx-runtime";
3
3
 
4
4
  //#region src/context/RouterContext.ts
@@ -8,6 +8,29 @@ const RouterContext = createContext(null);
8
8
  //#region src/context/RouteContext.ts
9
9
  const RouteContext = createContext(null);
10
10
 
11
+ //#endregion
12
+ //#region src/context/BlockerContext.ts
13
+ /**
14
+ * Create a new blocker registry.
15
+ * The registry manages registered blockers and provides a way to check if any blocker is active.
16
+ */
17
+ function createBlockerRegistry() {
18
+ const blockers = /* @__PURE__ */ new Map();
19
+ return {
20
+ register(id, shouldBlock) {
21
+ blockers.set(id, shouldBlock);
22
+ return () => {
23
+ blockers.delete(id);
24
+ };
25
+ },
26
+ checkAll() {
27
+ for (const shouldBlock of blockers.values()) if (shouldBlock()) return true;
28
+ return false;
29
+ }
30
+ };
31
+ }
32
+ const BlockerContext = createContext(null);
33
+
11
34
  //#endregion
12
35
  //#region src/types.ts
13
36
  /**
@@ -100,16 +123,16 @@ function matchPath(pattern, pathname, exact) {
100
123
  //#region src/core/loaderCache.ts
101
124
  /**
102
125
  * Cache for loader results.
103
- * Key format: `${entryId}:${routePath}`
126
+ * Key format: `${entryId}:${matchIndex}`
104
127
  */
105
128
  const loaderCache = /* @__PURE__ */ new Map();
106
129
  /**
107
130
  * Get or create a loader result from cache.
108
131
  * If the result is not cached, executes the loader and caches the result.
109
132
  */
110
- function getOrCreateLoaderResult(entryId, route$1, args) {
133
+ function getOrCreateLoaderResult(entryId, matchIndex, route$1, args) {
111
134
  if (!route$1.loader) return;
112
- const cacheKey = `${entryId}:${route$1.path}`;
135
+ const cacheKey = `${entryId}:${matchIndex}`;
113
136
  if (!loaderCache.has(cacheKey)) loaderCache.set(cacheKey, route$1.loader(args));
114
137
  return loaderCache.get(cacheKey);
115
138
  }
@@ -124,9 +147,9 @@ function createLoaderRequest(url) {
124
147
  * Results are cached by navigation entry id to prevent duplicate execution.
125
148
  */
126
149
  function executeLoaders(matchedRoutes, entryId, request, signal) {
127
- return matchedRoutes.map((match) => {
150
+ return matchedRoutes.map((match, index) => {
128
151
  const { route: route$1, params } = match;
129
- const data = getOrCreateLoaderResult(entryId, route$1, {
152
+ const data = getOrCreateLoaderResult(entryId, index, route$1, {
130
153
  params,
131
154
  request,
132
155
  signal
@@ -137,6 +160,14 @@ function executeLoaders(matchedRoutes, entryId, request, signal) {
137
160
  };
138
161
  });
139
162
  }
163
+ /**
164
+ * Clear loader cache entries for a specific navigation entry.
165
+ * Called when a NavigationHistoryEntry is disposed (removed from history stack).
166
+ */
167
+ function clearLoaderCacheForEntry(entryId) {
168
+ const prefix = `${entryId}:`;
169
+ for (const key of loaderCache.keys()) if (key.startsWith(prefix)) loaderCache.delete(key);
170
+ }
140
171
 
141
172
  //#endregion
142
173
  //#region src/core/NavigationAPIAdapter.ts
@@ -151,19 +182,21 @@ let idleController = null;
151
182
  * Adapter that uses the Navigation API for full SPA functionality.
152
183
  */
153
184
  var NavigationAPIAdapter = class {
154
- cachedSnapshot = null;
155
- cachedEntryId = null;
185
+ #cachedSnapshot = null;
186
+ #cachedEntryId = null;
187
+ #currentNavigationInfo = void 0;
156
188
  getSnapshot() {
157
189
  const entry = navigation.currentEntry;
158
190
  if (!entry?.url) return null;
159
- if (this.cachedEntryId === entry.id && this.cachedSnapshot) return this.cachedSnapshot;
160
- this.cachedEntryId = entry.id;
161
- this.cachedSnapshot = {
191
+ if (this.#cachedEntryId === entry.id && this.#cachedSnapshot) return this.#cachedSnapshot;
192
+ this.#cachedEntryId = entry.id;
193
+ this.#cachedSnapshot = {
162
194
  url: new URL(entry.url),
163
195
  key: entry.id,
164
- state: entry.getState()
196
+ state: entry.getState(),
197
+ info: this.#currentNavigationInfo
165
198
  };
166
- return this.cachedSnapshot;
199
+ return this.#cachedSnapshot;
167
200
  }
168
201
  getServerSnapshot() {
169
202
  return null;
@@ -171,38 +204,76 @@ var NavigationAPIAdapter = class {
171
204
  subscribe(callback) {
172
205
  const controller = new AbortController();
173
206
  navigation.addEventListener("currententrychange", callback, { signal: controller.signal });
207
+ this.#subscribeToDisposeEvents(controller.signal);
208
+ navigation.addEventListener("currententrychange", () => this.#subscribeToDisposeEvents(controller.signal), { signal: controller.signal });
174
209
  return () => {
175
210
  controller.abort();
176
211
  };
177
212
  }
213
+ /**
214
+ * Track which entries we've subscribed to dispose events for.
215
+ */
216
+ #subscribedEntryIds = /* @__PURE__ */ new Set();
217
+ /**
218
+ * Subscribe to dispose events on all navigation entries.
219
+ * When an entry is disposed, its cached loader results are cleared.
220
+ */
221
+ #subscribeToDisposeEvents(signal) {
222
+ for (const entry of navigation.entries()) {
223
+ if (this.#subscribedEntryIds.has(entry.id)) continue;
224
+ this.#subscribedEntryIds.add(entry.id);
225
+ const entryId = entry.id;
226
+ entry.addEventListener("dispose", () => {
227
+ clearLoaderCacheForEntry(entryId);
228
+ this.#subscribedEntryIds.delete(entryId);
229
+ }, { signal });
230
+ }
231
+ }
178
232
  navigate(to, options) {
179
233
  navigation.navigate(to, {
180
234
  history: options?.replace ? "replace" : "push",
181
- state: options?.state
235
+ state: options?.state,
236
+ info: options?.info
182
237
  });
183
238
  }
184
- setupInterception(routes, onNavigate) {
239
+ async navigateAsync(to, options) {
240
+ await navigation.navigate(to, {
241
+ history: options?.replace ? "replace" : "push",
242
+ state: options?.state,
243
+ info: options?.info
244
+ }).finished;
245
+ }
246
+ setupInterception(routes, onNavigate, checkBlockers) {
185
247
  const handleNavigate = (event) => {
186
- if (!event.canIntercept || event.hashChange) return;
248
+ this.#currentNavigationInfo = event.info;
249
+ this.#cachedSnapshot = null;
250
+ if (checkBlockers?.()) {
251
+ event.preventDefault();
252
+ return;
253
+ }
254
+ if (!event.canIntercept) {
255
+ onNavigate?.(event, []);
256
+ return;
257
+ }
187
258
  const url = new URL(event.destination.url);
188
259
  const matched = matchRoutes(routes, url.pathname);
189
260
  if (onNavigate) {
190
261
  onNavigate(event, matched);
191
262
  if (event.defaultPrevented) return;
192
263
  }
193
- if (matched) {
194
- if (idleController) {
195
- idleController.abort();
196
- idleController = null;
197
- }
198
- event.intercept({ handler: async () => {
199
- const request = createLoaderRequest(url);
200
- const currentEntry = navigation.currentEntry;
201
- if (!currentEntry) throw new Error("Navigation currentEntry is null during navigation interception");
202
- const results = executeLoaders(matched, currentEntry.id, request, event.signal);
203
- await Promise.all(results.map((r) => r.data));
204
- } });
264
+ if (event.hashChange || event.downloadRequest !== null) return;
265
+ if (!matched) return;
266
+ if (idleController) {
267
+ idleController.abort();
268
+ idleController = null;
205
269
  }
270
+ event.intercept({ handler: async () => {
271
+ const request = createLoaderRequest(url);
272
+ const currentEntry = navigation.currentEntry;
273
+ if (!currentEntry) throw new Error("Navigation currentEntry is null during navigation interception");
274
+ const results = executeLoaders(matched, currentEntry.id, request, event.signal);
275
+ await Promise.all(results.map((r) => r.data));
276
+ } });
206
277
  };
207
278
  const controller = new AbortController();
208
279
  navigation.addEventListener("navigate", handleNavigate, { signal: controller.signal });
@@ -214,6 +285,10 @@ var NavigationAPIAdapter = class {
214
285
  idleController ??= new AbortController();
215
286
  return idleController.signal;
216
287
  }
288
+ updateCurrentEntryState(state) {
289
+ this.#cachedSnapshot = null;
290
+ navigation.updateCurrentEntry({ state });
291
+ }
217
292
  };
218
293
 
219
294
  //#endregion
@@ -224,16 +299,17 @@ var NavigationAPIAdapter = class {
224
299
  * Links will cause full page loads (MPA behavior).
225
300
  */
226
301
  var StaticAdapter = class {
227
- cachedSnapshot = null;
228
- idleController = null;
302
+ #cachedSnapshot = null;
303
+ #idleController = null;
229
304
  getSnapshot() {
230
305
  if (typeof window === "undefined") return null;
231
- if (!this.cachedSnapshot) this.cachedSnapshot = {
306
+ if (!this.#cachedSnapshot) this.#cachedSnapshot = {
232
307
  url: new URL(window.location.href),
233
308
  key: "__static__",
234
- state: void 0
309
+ state: void 0,
310
+ info: void 0
235
311
  };
236
- return this.cachedSnapshot;
312
+ return this.#cachedSnapshot;
237
313
  }
238
314
  getServerSnapshot() {
239
315
  return null;
@@ -244,11 +320,15 @@ var StaticAdapter = class {
244
320
  navigate(to, _options) {
245
321
  console.warn("FUNSTACK Router: navigate() called in static fallback mode. Navigation API is not available in this browser. Links will cause full page loads.");
246
322
  }
247
- setupInterception(_routes, _onNavigate) {}
323
+ async navigateAsync(to, options) {
324
+ this.navigate(to, options);
325
+ }
326
+ setupInterception(_routes, _onNavigate, _checkBlockers) {}
248
327
  getIdleAbortSignal() {
249
- this.idleController ??= new AbortController();
250
- return this.idleController.signal;
328
+ this.#idleController ??= new AbortController();
329
+ return this.#idleController.signal;
251
330
  }
331
+ updateCurrentEntryState(_state) {}
252
332
  };
253
333
 
254
334
  //#endregion
@@ -258,7 +338,7 @@ var StaticAdapter = class {
258
338
  * All methods are no-ops that return safe default values.
259
339
  */
260
340
  var NullAdapter = class {
261
- idleController = null;
341
+ #idleController = null;
262
342
  getSnapshot() {
263
343
  return null;
264
344
  }
@@ -271,11 +351,15 @@ var NullAdapter = class {
271
351
  navigate(_to, _options) {
272
352
  console.warn("FUNSTACK Router: navigate() called but no adapter is available. Navigation API is not available in this browser and no fallback mode is configured.");
273
353
  }
274
- setupInterception(_routes, _onNavigate) {}
354
+ async navigateAsync(to, options) {
355
+ this.navigate(to, options);
356
+ }
357
+ setupInterception(_routes, _onNavigate, _checkBlockers) {}
275
358
  getIdleAbortSignal() {
276
- this.idleController ??= new AbortController();
277
- return this.idleController.signal;
359
+ this.#idleController ??= new AbortController();
360
+ return this.#idleController.signal;
278
361
  }
362
+ updateCurrentEntryState(_state) {}
279
363
  };
280
364
 
281
365
  //#endregion
@@ -304,17 +388,25 @@ function createAdapter(fallback) {
304
388
  function Router({ routes: inputRoutes, onNavigate, fallback = "none" }) {
305
389
  const routes = internalRoutes(inputRoutes);
306
390
  const adapter = useMemo(() => createAdapter(fallback), [fallback]);
391
+ const [blockerRegistry] = useState(() => createBlockerRegistry());
307
392
  const locationEntry = useSyncExternalStore(useCallback((callback) => adapter.subscribe(callback), [adapter]), () => adapter.getSnapshot(), () => adapter.getServerSnapshot());
308
393
  useEffect(() => {
309
- return adapter.setupInterception(routes, onNavigate);
394
+ return adapter.setupInterception(routes, onNavigate, blockerRegistry.checkAll);
310
395
  }, [
311
396
  adapter,
312
397
  routes,
313
- onNavigate
398
+ onNavigate,
399
+ blockerRegistry
314
400
  ]);
315
401
  const navigate = useCallback((to, options) => {
316
402
  adapter.navigate(to, options);
317
403
  }, [adapter]);
404
+ const navigateAsync = useCallback((to, options) => {
405
+ return adapter.navigateAsync(to, options);
406
+ }, [adapter]);
407
+ const updateCurrentEntryState = useCallback((state) => {
408
+ adapter.updateCurrentEntryState(state);
409
+ }, [adapter]);
318
410
  return useMemo(() => {
319
411
  if (locationEntry === null) return null;
320
412
  const { url, key } = locationEntry;
@@ -326,20 +418,29 @@ function Router({ routes: inputRoutes, onNavigate, fallback = "none" }) {
326
418
  const routerContextValue = {
327
419
  locationEntry,
328
420
  url,
329
- navigate
421
+ navigate,
422
+ navigateAsync,
423
+ updateCurrentEntryState
330
424
  };
331
- return /* @__PURE__ */ jsx(RouterContext.Provider, {
332
- value: routerContextValue,
333
- children: matchedRoutesWithData ? /* @__PURE__ */ jsx(RouteRenderer, {
334
- matchedRoutes: matchedRoutesWithData,
335
- index: 0
336
- }) : null
425
+ const blockerContextValue = { registry: blockerRegistry };
426
+ return /* @__PURE__ */ jsx(BlockerContext.Provider, {
427
+ value: blockerContextValue,
428
+ children: /* @__PURE__ */ jsx(RouterContext.Provider, {
429
+ value: routerContextValue,
430
+ children: matchedRoutesWithData ? /* @__PURE__ */ jsx(RouteRenderer, {
431
+ matchedRoutes: matchedRoutesWithData,
432
+ index: 0
433
+ }) : null
434
+ })
337
435
  });
338
436
  }, [
339
437
  navigate,
438
+ navigateAsync,
439
+ updateCurrentEntryState,
340
440
  locationEntry,
341
441
  routes,
342
- adapter
442
+ adapter,
443
+ blockerRegistry
343
444
  ]);
344
445
  }
345
446
  /**
@@ -350,6 +451,47 @@ function RouteRenderer({ matchedRoutes, index }) {
350
451
  if (!match) return null;
351
452
  const { route: route$1, params, pathname, data } = match;
352
453
  const Component = route$1.component;
454
+ const routerContext = useContext(RouterContext);
455
+ if (!routerContext) throw new Error("RouteRenderer must be used within RouterContext");
456
+ const { locationEntry, url, navigateAsync, updateCurrentEntryState } = routerContext;
457
+ const routeState$1 = locationEntry.state?.__routeStates?.[index];
458
+ const setStateSync = useCallback((stateOrUpdater) => {
459
+ const currentStates = locationEntry.state?.__routeStates ?? [];
460
+ const currentRouteState = currentStates[index];
461
+ const newState = typeof stateOrUpdater === "function" ? stateOrUpdater(currentRouteState) : stateOrUpdater;
462
+ const newStates = [...currentStates];
463
+ newStates[index] = newState;
464
+ updateCurrentEntryState({ __routeStates: newStates });
465
+ }, [
466
+ locationEntry.state,
467
+ index,
468
+ updateCurrentEntryState
469
+ ]);
470
+ const setState = useCallback(async (stateOrUpdater) => {
471
+ const currentStates = locationEntry.state?.__routeStates ?? [];
472
+ const currentRouteState = currentStates[index];
473
+ const newState = typeof stateOrUpdater === "function" ? stateOrUpdater(currentRouteState) : stateOrUpdater;
474
+ const newStates = [...currentStates];
475
+ newStates[index] = newState;
476
+ await navigateAsync(url.href, {
477
+ replace: true,
478
+ state: { __routeStates: newStates }
479
+ });
480
+ }, [
481
+ locationEntry.state,
482
+ index,
483
+ url,
484
+ navigateAsync
485
+ ]);
486
+ const resetState = useCallback(() => {
487
+ const newStates = [...locationEntry.state?.__routeStates ?? []];
488
+ newStates[index] = void 0;
489
+ updateCurrentEntryState({ __routeStates: newStates });
490
+ }, [
491
+ locationEntry.state,
492
+ index,
493
+ updateCurrentEntryState
494
+ ]);
353
495
  const outlet = index < matchedRoutes.length - 1 ? /* @__PURE__ */ jsx(RouteRenderer, {
354
496
  matchedRoutes,
355
497
  index: index + 1
@@ -365,11 +507,24 @@ function RouteRenderer({ matchedRoutes, index }) {
365
507
  ]);
366
508
  const renderComponent = () => {
367
509
  if (!Component) return outlet;
510
+ const stateProps = {
511
+ state: routeState$1,
512
+ setState,
513
+ setStateSync,
514
+ resetState
515
+ };
516
+ const { info } = locationEntry;
368
517
  if (route$1.loader) return /* @__PURE__ */ jsx(Component, {
369
518
  data,
370
- params
519
+ params,
520
+ ...stateProps,
521
+ info
522
+ });
523
+ return /* @__PURE__ */ jsx(Component, {
524
+ params,
525
+ ...stateProps,
526
+ info
371
527
  });
372
- return /* @__PURE__ */ jsx(Component, { params });
373
528
  };
374
529
  return /* @__PURE__ */ jsx(RouteContext.Provider, {
375
530
  value: routeContextValue,
@@ -418,17 +573,6 @@ function useLocation() {
418
573
  }, [url]);
419
574
  }
420
575
 
421
- //#endregion
422
- //#region src/hooks/useParams.ts
423
- /**
424
- * Returns route parameters from the matched path.
425
- */
426
- function useParams() {
427
- const context = useContext(RouteContext);
428
- if (!context) throw new Error("useParams must be used within a Router");
429
- return context.params;
430
- }
431
-
432
576
  //#endregion
433
577
  //#region src/hooks/useSearchParams.ts
434
578
  /**
@@ -450,12 +594,85 @@ function useSearchParams() {
450
594
  }, [context])];
451
595
  }
452
596
 
597
+ //#endregion
598
+ //#region src/hooks/useBlocker.ts
599
+ /**
600
+ * Hook to block navigation away from the current route.
601
+ *
602
+ * This is useful for scenarios like unsaved form data, ongoing file uploads,
603
+ * or any state that would be lost on navigation.
604
+ *
605
+ * @example
606
+ * ```tsx
607
+ * function EditForm() {
608
+ * const [isDirty, setIsDirty] = useState(false);
609
+ *
610
+ * useBlocker({
611
+ * shouldBlock: () => {
612
+ * if (isDirty) {
613
+ * return !confirm("You have unsaved changes. Leave anyway?");
614
+ * }
615
+ * return false;
616
+ * },
617
+ * });
618
+ *
619
+ * return <form>...</form>;
620
+ * }
621
+ * ```
622
+ *
623
+ * Note: This hook only handles SPA navigations (links, programmatic navigation).
624
+ * For hard navigations (tab close, refresh), handle `beforeunload` separately.
625
+ */
626
+ function useBlocker(options) {
627
+ const context = useContext(BlockerContext);
628
+ if (!context) throw new Error("useBlocker must be used within a Router");
629
+ const { shouldBlock } = options;
630
+ const blockerId = useId();
631
+ const { registry } = context;
632
+ useEffect(() => {
633
+ return registry.register(blockerId, shouldBlock);
634
+ }, [
635
+ blockerId,
636
+ shouldBlock,
637
+ registry
638
+ ]);
639
+ }
640
+
453
641
  //#endregion
454
642
  //#region src/route.ts
455
643
  function route(definition) {
456
644
  return definition;
457
645
  }
646
+ /**
647
+ * Helper function for creating type-safe route definitions with navigation state.
648
+ *
649
+ * Use this curried function when your route component needs to manage navigation state.
650
+ * The state is tied to the navigation history entry and persists across back/forward navigation.
651
+ *
652
+ * @example
653
+ * ```typescript
654
+ * // Route with navigation state
655
+ * type MyState = { scrollPosition: number };
656
+ * routeState<MyState>()({
657
+ * path: "users/:userId",
658
+ * component: UserPage, // Receives { params, state, setState, resetState }
659
+ * });
660
+ *
661
+ * // Route with both loader and navigation state
662
+ * type FilterState = { filter: string };
663
+ * routeState<FilterState>()({
664
+ * path: "products",
665
+ * loader: async () => fetchProducts(),
666
+ * component: ProductList, // Receives { data, params, state, setState, resetState }
667
+ * });
668
+ * ```
669
+ */
670
+ function routeState() {
671
+ return function(definition) {
672
+ return definition;
673
+ };
674
+ }
458
675
 
459
676
  //#endregion
460
- export { Outlet, Router, route, useLocation, useNavigate, useParams, useSearchParams };
677
+ export { Outlet, Router, route, routeState, useBlocker, useLocation, useNavigate, useSearchParams };
461
678
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["route","result: MatchedRoute","urlPatternPath: string","params: Record<string, string>","consumedPathname: string","route","idleController: AbortController | null","route","newParams: URLSearchParams"],"sources":["../src/context/RouterContext.ts","../src/context/RouteContext.ts","../src/types.ts","../src/core/matchRoutes.ts","../src/core/loaderCache.ts","../src/core/NavigationAPIAdapter.ts","../src/core/StaticAdapter.ts","../src/core/NullAdapter.ts","../src/core/createAdapter.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useParams.ts","../src/hooks/useSearchParams.ts","../src/route.ts"],"sourcesContent":["import { createContext } from \"react\";\nimport type { NavigateOptions } from \"../types.js\";\nimport type { LocationEntry } from \"../core/RouterAdapter.js\";\n\nexport type RouterContextValue = {\n /** Current location entry */\n locationEntry: LocationEntry;\n /** Current URL */\n url: URL;\n /** Navigate to a new URL */\n navigate: (to: string, options?: NavigateOptions) => void;\n};\n\nexport const RouterContext = createContext<RouterContextValue | null>(null);\n","import { createContext, type ReactNode } from \"react\";\n\nexport type RouteContextValue = {\n /** Matched route parameters */\n params: Record<string, string>;\n /** The matched path pattern */\n matchedPath: string;\n /** Child route element to render via Outlet */\n outlet: ReactNode;\n};\n\nexport const RouteContext = createContext<RouteContextValue | null>(null);\n","import type { ComponentType } from \"react\";\nimport type { LoaderArgs, RouteDefinition } from \"./route.js\";\n\nconst InternalRouteDefinitionSymbol = Symbol();\n\n/**\n * Route definition for the router.\n * When a loader is defined, the component receives the loader result as a `data` prop.\n */\nexport type InternalRouteDefinition = {\n [InternalRouteDefinitionSymbol]: never;\n /** Path pattern to match (e.g., \"users/:id\") */\n path: string;\n /** Child routes for nested routing */\n children?: InternalRouteDefinition[];\n\n // Note: `loader` and `component` may both exist or both not exist.\n // Also, `unknown`s may actually be more specific types. They are guaranteed\n // to be the same type by the `route` helper function.\n /** Data loader function for this route */\n loader?: (args: LoaderArgs) => unknown;\n /** Component to render when this route matches */\n component?: ComponentType<{\n data?: unknown;\n params?: Record<string, string>;\n }>;\n};\n\n/**\n * Converts user-defined routes to internal route definitions.\n * This function is used internally by the Router.\n *\n * Actually, this function just performs a type assertion since\n * both RouteDefinition and InternalRouteDefinition have the same runtime shape.\n */\nexport function internalRoutes(\n routes: RouteDefinition[],\n): InternalRouteDefinition[] {\n return routes as InternalRouteDefinition[];\n}\n\n/**\n * A matched route with its parameters.\n */\nexport type MatchedRoute = {\n /** The original route definition */\n route: InternalRouteDefinition;\n /** Extracted path parameters */\n params: Record<string, string>;\n /** The matched pathname segment */\n pathname: string;\n};\n\n/**\n * A matched route with loader data.\n */\nexport type MatchedRouteWithData = MatchedRoute & {\n /** Data returned from the loader (undefined if no loader) */\n data: unknown | undefined;\n};\n\n/**\n * Options for navigation.\n */\nexport type NavigateOptions = {\n /** Replace current history entry instead of pushing */\n replace?: boolean;\n /** State to associate with the navigation */\n state?: unknown;\n};\n\n/**\n * Location object representing current URL state.\n */\nexport type Location = {\n pathname: string;\n search: string;\n hash: string;\n};\n\n/**\n * Callback invoked before navigation is intercepted.\n * Call `event.preventDefault()` to prevent the router from handling this navigation.\n *\n * @param event - The NavigateEvent from the Navigation API\n * @param matched - Array of matched routes, or null if no routes matched\n */\nexport type OnNavigateCallback = (\n event: NavigateEvent,\n matched: readonly MatchedRoute[] | null,\n) => void;\n\n/**\n * Fallback mode when Navigation API is unavailable.\n *\n * - `\"none\"` (default): Render nothing when Navigation API is unavailable\n * - `\"static\"`: Render matched routes without navigation capabilities (MPA behavior)\n */\nexport type FallbackMode =\n | \"none\" // Default: render nothing when Navigation API unavailable\n | \"static\"; // Render matched routes without navigation capabilities\n","import type { InternalRouteDefinition, MatchedRoute } from \"../types.js\";\n\n/**\n * Match a pathname against a route tree, returning the matched route stack.\n * Returns null if no match is found.\n */\nexport function matchRoutes(\n routes: InternalRouteDefinition[],\n pathname: string,\n): MatchedRoute[] | null {\n for (const route of routes) {\n const matched = matchRoute(route, pathname);\n if (matched) {\n return matched;\n }\n }\n return null;\n}\n\n/**\n * Match a single route and its children recursively.\n */\nfunction matchRoute(\n route: InternalRouteDefinition,\n pathname: string,\n): MatchedRoute[] | null {\n const hasChildren = Boolean(route.children?.length);\n\n // For parent routes (with children), we need to match as a prefix\n // For leaf routes (no children), we need an exact match\n const { matched, params, consumedPathname } = matchPath(\n route.path,\n pathname,\n !hasChildren,\n );\n\n if (!matched) {\n return null;\n }\n\n const result: MatchedRoute = {\n route,\n params,\n pathname: consumedPathname,\n };\n\n // If this route has children, try to match them\n if (hasChildren) {\n // Calculate remaining pathname, ensuring it starts with /\n let remainingPathname = pathname.slice(consumedPathname.length);\n if (!remainingPathname.startsWith(\"/\")) {\n remainingPathname = \"/\" + remainingPathname;\n }\n if (remainingPathname === \"\") {\n remainingPathname = \"/\";\n }\n\n for (const child of route.children!) {\n const childMatch = matchRoute(child, remainingPathname);\n if (childMatch) {\n // Merge params from parent into children\n return [\n result,\n ...childMatch.map((m) => ({\n ...m,\n params: { ...params, ...m.params },\n })),\n ];\n }\n }\n\n // If no children matched but this route has a component, it's still a valid match\n if (route.component) {\n return [result];\n }\n\n return null;\n }\n\n return [result];\n}\n\n/**\n * Match a path pattern against a pathname.\n */\nfunction matchPath(\n pattern: string,\n pathname: string,\n exact: boolean,\n): {\n matched: boolean;\n params: Record<string, string>;\n consumedPathname: string;\n} {\n // Normalize pattern\n const normalizedPattern = pattern.startsWith(\"/\") ? pattern : `/${pattern}`;\n\n // Build URLPattern\n let urlPatternPath: string;\n if (exact) {\n urlPatternPath = normalizedPattern;\n } else if (normalizedPattern === \"/\") {\n // Special case: root path as prefix matches anything\n urlPatternPath = \"/*\";\n } else {\n // For other prefix matches, add optional wildcard suffix\n urlPatternPath = `${normalizedPattern}{/*}?`;\n }\n\n const urlPattern = new URLPattern({ pathname: urlPatternPath });\n\n const match = urlPattern.exec({ pathname });\n if (!match) {\n return { matched: false, params: {}, consumedPathname: \"\" };\n }\n\n // Extract params (excluding the wildcard group \"0\")\n const params: Record<string, string> = {};\n for (const [key, value] of Object.entries(match.pathname.groups)) {\n if (value !== undefined && key !== \"0\") {\n params[key] = value;\n }\n }\n\n // Calculate consumed pathname\n let consumedPathname: string;\n if (exact) {\n consumedPathname = pathname;\n } else if (normalizedPattern === \"/\") {\n // Root pattern consumes just \"/\"\n consumedPathname = \"/\";\n } else {\n // For prefix matches, calculate based on pattern segments\n const patternSegments = normalizedPattern.split(\"/\").filter(Boolean);\n const pathnameSegments = pathname.split(\"/\").filter(Boolean);\n consumedPathname =\n \"/\" + pathnameSegments.slice(0, patternSegments.length).join(\"/\");\n }\n\n return { matched: true, params, consumedPathname };\n}\n","import type { LoaderArgs } from \"../route.js\";\nimport type {\n MatchedRoute,\n MatchedRouteWithData,\n InternalRouteDefinition,\n} from \"../types.js\";\n\n/**\n * Cache for loader results.\n * Key format: `${entryId}:${routePath}`\n */\nconst loaderCache = new Map<string, unknown>();\n\n/**\n * Get or create a loader result from cache.\n * If the result is not cached, executes the loader and caches the result.\n */\nfunction getOrCreateLoaderResult(\n entryId: string,\n route: InternalRouteDefinition,\n args: LoaderArgs,\n): unknown | undefined {\n if (!route.loader) {\n return undefined;\n }\n\n const cacheKey = `${entryId}:${route.path}`;\n\n if (!loaderCache.has(cacheKey)) {\n loaderCache.set(cacheKey, route.loader(args));\n }\n\n return loaderCache.get(cacheKey);\n}\n\n/**\n * Create a Request object for loader args.\n */\nexport function createLoaderRequest(url: URL): Request {\n return new Request(url.href, {\n method: \"GET\",\n });\n}\n\n/**\n * Execute loaders for matched routes and return routes with data.\n * Results are cached by navigation entry id to prevent duplicate execution.\n */\nexport function executeLoaders(\n matchedRoutes: MatchedRoute[],\n entryId: string,\n request: Request,\n signal: AbortSignal,\n): MatchedRouteWithData[] {\n return matchedRoutes.map((match) => {\n const { route, params } = match;\n const args: LoaderArgs = { params, request, signal };\n const data = getOrCreateLoaderResult(entryId, route, args);\n\n return { ...match, data };\n });\n}\n\n/**\n * Clear the loader cache.\n * Mainly used for testing.\n */\nexport function clearLoaderCache(): void {\n loaderCache.clear();\n}\n","import type { RouterAdapter, LocationEntry } from \"./RouterAdapter.js\";\nimport type {\n InternalRouteDefinition,\n NavigateOptions,\n OnNavigateCallback,\n} from \"../types.js\";\nimport { matchRoutes } from \"./matchRoutes.js\";\nimport { executeLoaders, createLoaderRequest } from \"./loaderCache.js\";\n\n/**\n * Fallback AbortController for data loading initialized outside of a navigation event.\n * Aborted when the next navigation occurs.\n *\n * To save resources, this controller is created only when needed.\n */\nlet idleController: AbortController | null = null;\n\n/**\n * Reset navigation state. Used for testing.\n */\nexport function resetNavigationState(): void {\n idleController?.abort();\n idleController = null;\n}\n\n/**\n * Adapter that uses the Navigation API for full SPA functionality.\n */\nexport class NavigationAPIAdapter implements RouterAdapter {\n // Cache the snapshot to ensure referential stability for useSyncExternalStore\n private cachedSnapshot: LocationEntry | null = null;\n private cachedEntryId: string | null = null;\n\n getSnapshot(): LocationEntry | null {\n const entry = navigation.currentEntry;\n if (!entry?.url) {\n return null;\n }\n\n // Return cached snapshot if entry hasn't changed\n if (this.cachedEntryId === entry.id && this.cachedSnapshot) {\n return this.cachedSnapshot;\n }\n\n // Create new snapshot and cache it\n this.cachedEntryId = entry.id;\n this.cachedSnapshot = {\n url: new URL(entry.url),\n key: entry.id,\n state: entry.getState(),\n };\n return this.cachedSnapshot;\n }\n\n getServerSnapshot(): LocationEntry | null {\n return null;\n }\n\n subscribe(callback: () => void): () => void {\n const controller = new AbortController();\n navigation.addEventListener(\"currententrychange\", callback, {\n signal: controller.signal,\n });\n return () => {\n controller.abort();\n };\n }\n\n navigate(to: string, options?: NavigateOptions): void {\n navigation.navigate(to, {\n history: options?.replace ? \"replace\" : \"push\",\n state: options?.state,\n });\n }\n\n setupInterception(\n routes: InternalRouteDefinition[],\n onNavigate?: OnNavigateCallback,\n ): (() => void) | undefined {\n const handleNavigate = (event: NavigateEvent) => {\n // Only intercept same-origin navigations\n if (!event.canIntercept || event.hashChange) {\n return;\n }\n\n // Check if the URL matches any of our routes\n const url = new URL(event.destination.url);\n const matched = matchRoutes(routes, url.pathname);\n\n // Call onNavigate callback if provided (regardless of route match)\n if (onNavigate) {\n onNavigate(event, matched);\n if (event.defaultPrevented) {\n return; // Do not intercept, allow browser default\n }\n }\n\n if (matched) {\n // Abort initial load's loaders if this is the first navigation\n if (idleController) {\n idleController.abort();\n idleController = null;\n }\n\n event.intercept({\n handler: async () => {\n const request = createLoaderRequest(url);\n\n // Note: in response to `currententrychange` event, <Router> should already\n // have dispatched data loaders and the results should be cached.\n // Here we run executeLoader to retrieve cached results.\n const currentEntry = navigation.currentEntry;\n if (!currentEntry) {\n throw new Error(\n \"Navigation currentEntry is null during navigation interception\",\n );\n }\n\n const results = executeLoaders(\n matched,\n currentEntry.id,\n request,\n event.signal,\n );\n\n // Delay navigation until async loaders complete\n await Promise.all(results.map((r) => r.data));\n },\n });\n }\n };\n\n const controller = new AbortController();\n navigation.addEventListener(\"navigate\", handleNavigate, {\n signal: controller.signal,\n });\n return () => {\n controller.abort();\n };\n }\n\n getIdleAbortSignal(): AbortSignal {\n idleController ??= new AbortController();\n return idleController.signal;\n }\n}\n","import type { RouterAdapter, LocationEntry } from \"./RouterAdapter.js\";\nimport type {\n InternalRouteDefinition,\n NavigateOptions,\n OnNavigateCallback,\n} from \"../types.js\";\n\n/**\n * Static adapter for fallback mode when Navigation API is unavailable.\n * Provides read-only location access with no SPA navigation capabilities.\n * Links will cause full page loads (MPA behavior).\n */\nexport class StaticAdapter implements RouterAdapter {\n private cachedSnapshot: LocationEntry | null = null;\n private idleController: AbortController | null = null;\n\n getSnapshot(): LocationEntry | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n // Cache the snapshot - it never changes in static mode\n if (!this.cachedSnapshot) {\n this.cachedSnapshot = {\n url: new URL(window.location.href),\n key: \"__static__\",\n state: undefined,\n };\n }\n return this.cachedSnapshot;\n }\n\n getServerSnapshot(): LocationEntry | null {\n return null;\n }\n\n subscribe(_callback: () => void): () => void {\n // Static mode never fires location change events\n return () => {};\n }\n\n navigate(to: string, _options?: NavigateOptions): void {\n console.warn(\n \"FUNSTACK Router: navigate() called in static fallback mode. \" +\n \"Navigation API is not available in this browser. \" +\n \"Links will cause full page loads.\",\n );\n // Note: We intentionally do NOT do window.location.href = to\n // as that would mask bugs where developers expect SPA behavior.\n // If needed in the future, we could add a \"static-reload\" mode.\n }\n\n setupInterception(\n _routes: InternalRouteDefinition[],\n _onNavigate?: OnNavigateCallback,\n ): (() => void) | undefined {\n // No interception in static mode - links cause full page loads\n return undefined;\n }\n\n getIdleAbortSignal(): AbortSignal {\n this.idleController ??= new AbortController();\n return this.idleController.signal;\n }\n}\n","import type { RouterAdapter, LocationEntry } from \"./RouterAdapter.js\";\nimport type {\n InternalRouteDefinition,\n NavigateOptions,\n OnNavigateCallback,\n} from \"../types.js\";\n\n/**\n * Null adapter for when Navigation API is unavailable and no fallback is configured.\n * All methods are no-ops that return safe default values.\n */\nexport class NullAdapter implements RouterAdapter {\n private idleController: AbortController | null = null;\n\n getSnapshot(): LocationEntry | null {\n return null;\n }\n\n getServerSnapshot(): LocationEntry | null {\n return null;\n }\n\n subscribe(_callback: () => void): () => void {\n return () => {};\n }\n\n navigate(_to: string, _options?: NavigateOptions): void {\n console.warn(\n \"FUNSTACK Router: navigate() called but no adapter is available. \" +\n \"Navigation API is not available in this browser and no fallback mode is configured.\",\n );\n }\n\n setupInterception(\n _routes: InternalRouteDefinition[],\n _onNavigate?: OnNavigateCallback,\n ): (() => void) | undefined {\n return undefined;\n }\n\n getIdleAbortSignal(): AbortSignal {\n this.idleController ??= new AbortController();\n return this.idleController.signal;\n }\n}\n","import type { RouterAdapter } from \"./RouterAdapter.js\";\nimport { NavigationAPIAdapter } from \"./NavigationAPIAdapter.js\";\nimport { StaticAdapter } from \"./StaticAdapter.js\";\nimport { NullAdapter } from \"./NullAdapter.js\";\nimport type { FallbackMode } from \"../types.js\";\n\n/**\n * Check if Navigation API is available.\n */\nfunction hasNavigation(): boolean {\n return typeof window !== \"undefined\" && \"navigation\" in window;\n}\n\n/**\n * Create the appropriate router adapter based on browser capabilities\n * and the specified fallback mode.\n *\n * @param fallback - The fallback mode to use when Navigation API is unavailable\n * @returns A RouterAdapter instance\n */\nexport function createAdapter(fallback: FallbackMode): RouterAdapter {\n // Try Navigation API first\n if (hasNavigation()) {\n return new NavigationAPIAdapter();\n }\n\n // Fall back to static mode if enabled\n if (fallback === \"static\") {\n return new StaticAdapter();\n }\n\n // No adapter available (fallback=\"none\" or default)\n return new NullAdapter();\n}\n","import {\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useSyncExternalStore,\n} from \"react\";\nimport { RouterContext } from \"./context/RouterContext.js\";\nimport { RouteContext } from \"./context/RouteContext.js\";\nimport {\n type NavigateOptions,\n type MatchedRouteWithData,\n type OnNavigateCallback,\n type FallbackMode,\n internalRoutes,\n} from \"./types.js\";\nimport { matchRoutes } from \"./core/matchRoutes.js\";\nimport { createAdapter } from \"./core/createAdapter.js\";\nimport { executeLoaders, createLoaderRequest } from \"./core/loaderCache.js\";\nimport type { RouteDefinition } from \"./route.js\";\n\nexport type RouterProps = {\n routes: RouteDefinition[];\n /**\n * Callback invoked before navigation is intercepted.\n * Call `event.preventDefault()` to prevent the router from handling this navigation.\n *\n * @param event - The NavigateEvent from the Navigation API\n * @param matched - Array of matched routes, or null if no routes matched\n */\n onNavigate?: OnNavigateCallback;\n /**\n * Fallback mode when Navigation API is unavailable.\n *\n * - `\"none\"` (default): Render nothing when Navigation API is unavailable\n * - `\"static\"`: Render matched routes without navigation capabilities (MPA behavior)\n */\n fallback?: FallbackMode;\n};\n\nexport function Router({\n routes: inputRoutes,\n onNavigate,\n fallback = \"none\",\n}: RouterProps): ReactNode {\n const routes = internalRoutes(inputRoutes);\n\n // Create adapter once based on browser capabilities and fallback setting\n const adapter = useMemo(() => createAdapter(fallback), [fallback]);\n\n // Subscribe to location changes via adapter\n const locationEntry = useSyncExternalStore(\n useCallback((callback) => adapter.subscribe(callback), [adapter]),\n () => adapter.getSnapshot(),\n () => adapter.getServerSnapshot(),\n );\n\n // Set up navigation interception via adapter\n useEffect(() => {\n return adapter.setupInterception(routes, onNavigate);\n }, [adapter, routes, onNavigate]);\n\n // Navigate function from adapter\n const navigate = useCallback(\n (to: string, options?: NavigateOptions) => {\n adapter.navigate(to, options);\n },\n [adapter],\n );\n\n return useMemo(() => {\n if (locationEntry === null) {\n // This happens either when Navigation API is unavailable (and no fallback),\n // or the current document is not fully active.\n return null;\n }\n\n const { url, key } = locationEntry;\n\n // Match current URL against routes and execute loaders\n const matchedRoutesWithData = (() => {\n const matched = matchRoutes(routes, url.pathname);\n if (!matched) return null;\n\n // Execute loaders (results are cached by location entry key)\n const request = createLoaderRequest(url);\n const signal = adapter.getIdleAbortSignal();\n return executeLoaders(matched, key, request, signal);\n })();\n\n const routerContextValue = { locationEntry, url, navigate };\n\n return (\n <RouterContext.Provider value={routerContextValue}>\n {matchedRoutesWithData ? (\n <RouteRenderer matchedRoutes={matchedRoutesWithData} index={0} />\n ) : null}\n </RouterContext.Provider>\n );\n }, [navigate, locationEntry, routes, adapter]);\n}\n\ntype RouteRendererProps = {\n matchedRoutes: MatchedRouteWithData[];\n index: number;\n};\n\n/**\n * Recursively render matched routes with proper context.\n */\nfunction RouteRenderer({\n matchedRoutes,\n index,\n}: RouteRendererProps): ReactNode {\n const match = matchedRoutes[index];\n if (!match) return null;\n\n const { route, params, pathname, data } = match;\n const Component = route.component;\n\n // Create outlet for child routes\n const outlet =\n index < matchedRoutes.length - 1 ? (\n <RouteRenderer matchedRoutes={matchedRoutes} index={index + 1} />\n ) : null;\n\n const routeContextValue = useMemo(\n () => ({ params, matchedPath: pathname, outlet }),\n [params, pathname, outlet],\n );\n\n // Render component with or without data prop based on loader presence\n // Always pass params prop to components\n const renderComponent = () => {\n if (!Component) return outlet;\n\n // When loader exists, data is defined and component expects data prop\n // When loader doesn't exist, data is undefined and component doesn't expect data prop\n // TypeScript can't narrow this union, so we use runtime check with type assertion\n if (route.loader) {\n const ComponentWithData = Component as React.ComponentType<{\n data: unknown;\n params: Record<string, string>;\n }>;\n return <ComponentWithData data={data} params={params} />;\n }\n const ComponentWithoutData = Component as React.ComponentType<{\n params: Record<string, string>;\n }>;\n return <ComponentWithoutData params={params} />;\n };\n\n return (\n <RouteContext.Provider value={routeContextValue}>\n {renderComponent()}\n </RouteContext.Provider>\n );\n}\n","import { type ReactNode, useContext } from \"react\";\nimport { RouteContext } from \"./context/RouteContext.js\";\n\n/**\n * Renders the matched child route.\n * Used in layout components to specify where child routes should render.\n */\nexport function Outlet(): ReactNode {\n const routeContext = useContext(RouteContext);\n\n if (!routeContext) {\n return null;\n }\n\n return routeContext.outlet;\n}\n","import { useContext } from \"react\";\nimport { RouterContext } from \"../context/RouterContext.js\";\nimport type { NavigateOptions } from \"../types.js\";\n\n/**\n * Returns a function for programmatic navigation.\n */\nexport function useNavigate(): (to: string, options?: NavigateOptions) => void {\n const context = useContext(RouterContext);\n\n if (!context) {\n throw new Error(\"useNavigate must be used within a Router\");\n }\n\n return context.navigate;\n}\n","import { useContext, useMemo } from \"react\";\nimport { RouterContext } from \"../context/RouterContext.js\";\nimport type { Location } from \"../types.js\";\n\n/**\n * Returns the current location object.\n */\nexport function useLocation(): Location {\n const context = useContext(RouterContext);\n\n if (!context) {\n throw new Error(\"useLocation must be used within a Router\");\n }\n\n const { url } = context;\n\n return useMemo(() => {\n return {\n pathname: url.pathname,\n search: url.search,\n hash: url.hash,\n };\n }, [url]);\n}\n","import { useContext } from \"react\";\nimport { RouteContext } from \"../context/RouteContext.js\";\n\n/**\n * Returns route parameters from the matched path.\n */\nexport function useParams<\n T extends Record<string, string> = Record<string, string>,\n>(): T {\n const context = useContext(RouteContext);\n\n if (!context) {\n throw new Error(\"useParams must be used within a Router\");\n }\n\n return context.params as T;\n}\n","import { useCallback, useContext, useMemo } from \"react\";\nimport { RouterContext } from \"../context/RouterContext.js\";\n\ntype SetSearchParams = (\n params:\n | URLSearchParams\n | Record<string, string>\n | ((prev: URLSearchParams) => URLSearchParams | Record<string, string>),\n) => void;\n\n/**\n * Returns and allows manipulation of URL search parameters.\n */\nexport function useSearchParams(): [URLSearchParams, SetSearchParams] {\n const context = useContext(RouterContext);\n\n if (!context) {\n throw new Error(\"useSearchParams must be used within a Router\");\n }\n\n const searchParams = context.url.searchParams;\n\n const setSearchParams = useCallback<SetSearchParams>(\n (params) => {\n const url = new URL(context.url);\n\n let newParams: URLSearchParams;\n if (typeof params === \"function\") {\n const result = params(new URLSearchParams(url.search));\n newParams =\n result instanceof URLSearchParams\n ? result\n : new URLSearchParams(result);\n } else if (params instanceof URLSearchParams) {\n newParams = params;\n } else {\n newParams = new URLSearchParams(params);\n }\n\n url.search = newParams.toString();\n context.navigate(url.pathname + url.search + url.hash, { replace: true });\n },\n [context],\n );\n\n return [searchParams, setSearchParams];\n}\n","import type { ComponentType } from \"react\";\n\nconst routeDefinitionSymbol = Symbol();\n\n/**\n * Extracts parameter names from a path pattern.\n * E.g., \"/users/:id/posts/:postId\" -> \"id\" | \"postId\"\n */\ntype ExtractParams<T extends string> =\n T extends `${string}:${infer Param}/${infer Rest}`\n ? Param | ExtractParams<`/${Rest}`>\n : T extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * Creates a params object type from a path pattern.\n * E.g., \"/users/:id\" -> { id: string }\n */\nexport type PathParams<T extends string> = [ExtractParams<T>] extends [never]\n ? Record<string, never>\n : { [K in ExtractParams<T>]: string };\n\n/**\n * Arguments passed to loader functions.\n */\nexport type LoaderArgs = {\n /** Extracted path parameters */\n params: Record<string, string>;\n /** Request object with URL and headers */\n request: Request;\n /** AbortSignal for cancellation on navigation */\n signal: AbortSignal;\n};\n\n/**\n * Route definition created by the `route` helper function.\n */\nexport interface OpaqueRouteDefinition {\n [routeDefinitionSymbol]: never;\n path: string;\n children?: RouteDefinition[];\n}\n\n/**\n * Any route definition defined by user.\n */\nexport type RouteDefinition =\n | OpaqueRouteDefinition\n | {\n path: string;\n component?: ComponentType<{}>;\n children?: RouteDefinition[];\n };\n\n/**\n * Route definition with loader - infers TData from loader return type.\n * TPath is used to infer params type from the path pattern.\n */\ntype RouteWithLoader<TPath extends string, TData> = {\n path: TPath;\n loader: (args: LoaderArgs) => TData;\n component: ComponentType<{ data: TData; params: PathParams<TPath> }>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n children?: RouteDefinition[];\n};\n\n/**\n * Route definition without loader.\n * TPath is used to infer params type from the path pattern.\n */\ntype RouteWithoutLoader<TPath extends string> = {\n path: TPath;\n component?: ComponentType<{ params: PathParams<TPath> }>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n children?: RouteDefinition[];\n};\n\n/**\n * Helper function for creating type-safe route definitions.\n *\n * When a loader is provided, TypeScript infers the return type and ensures\n * the component accepts a `data` prop of that type. Components always receive\n * a `params` prop with types inferred from the path pattern.\n *\n * @example\n * ```typescript\n * // Route with async loader\n * route({\n * path: \"users/:userId\",\n * loader: async ({ params, signal }) => {\n * const res = await fetch(`/api/users/${params.userId}`, { signal });\n * return res.json() as Promise<User>;\n * },\n * component: UserDetail, // Must accept { data: Promise<User>, params: { userId: string } }\n * });\n *\n * // Route without loader\n * route({\n * path: \"about\",\n * component: AboutPage, // Must accept { params: {} }\n * });\n * ```\n */\nexport function route<TPath extends string, TData>(\n definition: RouteWithLoader<TPath, TData>,\n): OpaqueRouteDefinition;\nexport function route<TPath extends string>(\n definition: RouteWithoutLoader<TPath>,\n): OpaqueRouteDefinition;\nexport function route<TPath extends string, TData>(\n definition: RouteWithLoader<TPath, TData> | RouteWithoutLoader<TPath>,\n): OpaqueRouteDefinition {\n return definition as unknown as OpaqueRouteDefinition;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["route","result: MatchedRoute","urlPatternPath: string","params: Record<string, string>","consumedPathname: string","route","idleController: AbortController | null","#cachedEntryId","#cachedSnapshot","#currentNavigationInfo","#subscribeToDisposeEvents","#subscribedEntryIds","#cachedSnapshot","#idleController","#idleController","route","routeState","newParams: URLSearchParams"],"sources":["../src/context/RouterContext.ts","../src/context/RouteContext.ts","../src/context/BlockerContext.ts","../src/types.ts","../src/core/matchRoutes.ts","../src/core/loaderCache.ts","../src/core/NavigationAPIAdapter.ts","../src/core/StaticAdapter.ts","../src/core/NullAdapter.ts","../src/core/createAdapter.ts","../src/Router.tsx","../src/Outlet.tsx","../src/hooks/useNavigate.ts","../src/hooks/useLocation.ts","../src/hooks/useSearchParams.ts","../src/hooks/useBlocker.ts","../src/route.ts"],"sourcesContent":["import { createContext } from \"react\";\nimport type { NavigateOptions } from \"../types.js\";\nimport type { LocationEntry } from \"../core/RouterAdapter.js\";\n\nexport type RouterContextValue = {\n /** Current location entry */\n locationEntry: LocationEntry;\n /** Current URL */\n url: URL;\n /** Navigate to a new URL */\n navigate: (to: string, options?: NavigateOptions) => void;\n /** Navigate to a new URL and wait for completion */\n navigateAsync: (to: string, options?: NavigateOptions) => Promise<void>;\n /** Update current entry's state without navigation */\n updateCurrentEntryState: (state: unknown) => void;\n};\n\nexport const RouterContext = createContext<RouterContextValue | null>(null);\n","import { createContext, type ReactNode } from \"react\";\n\nexport type RouteContextValue = {\n /** Matched route parameters */\n params: Record<string, string>;\n /** The matched path pattern */\n matchedPath: string;\n /** Child route element to render via Outlet */\n outlet: ReactNode;\n};\n\nexport const RouteContext = createContext<RouteContextValue | null>(null);\n","import { createContext } from \"react\";\n\nexport type BlockerId = string;\n\nexport type BlockerRegistry = {\n /** Register a blocker, returns unregister function */\n register: (id: BlockerId, shouldBlock: () => boolean) => () => void;\n /** Check all blockers - returns true if any blocks */\n checkAll: () => boolean;\n};\n\nexport type BlockerContextValue = {\n registry: BlockerRegistry;\n};\n\n/**\n * Create a new blocker registry.\n * The registry manages registered blockers and provides a way to check if any blocker is active.\n */\nexport function createBlockerRegistry(): BlockerRegistry {\n const blockers = new Map<BlockerId, () => boolean>();\n\n return {\n register(id: BlockerId, shouldBlock: () => boolean): () => void {\n blockers.set(id, shouldBlock);\n return () => {\n blockers.delete(id);\n };\n },\n\n checkAll(): boolean {\n for (const shouldBlock of blockers.values()) {\n if (shouldBlock()) {\n return true;\n }\n }\n return false;\n },\n };\n}\n\nexport const BlockerContext = createContext<BlockerContextValue | null>(null);\n","import type { ComponentType } from \"react\";\nimport type { LoaderArgs, RouteDefinition } from \"./route.js\";\n\nconst InternalRouteDefinitionSymbol = Symbol();\n\n/**\n * Internal structure for storing per-route state in NavigationHistoryEntry.\n * Each route in the matched stack gets its own state slot indexed by match position.\n */\nexport type InternalRouteState = {\n __routeStates: (unknown | undefined)[];\n};\n\n/**\n * Route definition for the router.\n * When a loader is defined, the component receives the loader result as a `data` prop.\n */\nexport type InternalRouteDefinition = {\n [InternalRouteDefinitionSymbol]: never;\n /** Path pattern to match (e.g., \"users/:id\") */\n path: string;\n /** Child routes for nested routing */\n children?: InternalRouteDefinition[];\n\n // Note: `loader` and `component` may both exist or both not exist.\n // Also, `unknown`s may actually be more specific types. They are guaranteed\n // to be the same type by the `route` helper function.\n /** Data loader function for this route */\n loader?: (args: LoaderArgs) => unknown;\n /** Component to render when this route matches */\n component?: ComponentType<{\n data?: unknown;\n params?: Record<string, string>;\n state?: unknown;\n setState?: (state: unknown | ((prev: unknown) => unknown)) => Promise<void>;\n setStateSync?: (state: unknown | ((prev: unknown) => unknown)) => void;\n resetState?: () => void;\n info?: unknown;\n }>;\n};\n\n/**\n * Converts user-defined routes to internal route definitions.\n * This function is used internally by the Router.\n *\n * Actually, this function just performs a type assertion since\n * both RouteDefinition and InternalRouteDefinition have the same runtime shape.\n */\nexport function internalRoutes(\n routes: RouteDefinition[],\n): InternalRouteDefinition[] {\n return routes as InternalRouteDefinition[];\n}\n\n/**\n * A matched route with its parameters.\n */\nexport type MatchedRoute = {\n /** The original route definition */\n route: InternalRouteDefinition;\n /** Extracted path parameters */\n params: Record<string, string>;\n /** The matched pathname segment */\n pathname: string;\n};\n\n/**\n * A matched route with loader data.\n */\nexport type MatchedRouteWithData = MatchedRoute & {\n /** Data returned from the loader (undefined if no loader) */\n data: unknown | undefined;\n};\n\n/**\n * Options for navigation.\n */\nexport type NavigateOptions = {\n /** Replace current history entry instead of pushing */\n replace?: boolean;\n /** State to associate with the navigation */\n state?: unknown;\n /** Ephemeral info for this navigation only (not persisted in history) */\n info?: unknown;\n};\n\n/**\n * Location object representing current URL state.\n */\nexport type Location = {\n pathname: string;\n search: string;\n hash: string;\n};\n\n/**\n * Callback invoked before navigation is intercepted.\n * Call `event.preventDefault()` to prevent the router from handling this navigation.\n *\n * @param event - The NavigateEvent from the Navigation API\n * @param matched - Array of matched routes, or null if no routes matched\n */\nexport type OnNavigateCallback = (\n event: NavigateEvent,\n matched: readonly MatchedRoute[] | null,\n) => void;\n\n/**\n * Fallback mode when Navigation API is unavailable.\n *\n * - `\"none\"` (default): Render nothing when Navigation API is unavailable\n * - `\"static\"`: Render matched routes without navigation capabilities (MPA behavior)\n */\nexport type FallbackMode =\n | \"none\" // Default: render nothing when Navigation API unavailable\n | \"static\"; // Render matched routes without navigation capabilities\n","import type { InternalRouteDefinition, MatchedRoute } from \"../types.js\";\n\n/**\n * Match a pathname against a route tree, returning the matched route stack.\n * Returns null if no match is found.\n */\nexport function matchRoutes(\n routes: InternalRouteDefinition[],\n pathname: string,\n): MatchedRoute[] | null {\n for (const route of routes) {\n const matched = matchRoute(route, pathname);\n if (matched) {\n return matched;\n }\n }\n return null;\n}\n\n/**\n * Match a single route and its children recursively.\n */\nfunction matchRoute(\n route: InternalRouteDefinition,\n pathname: string,\n): MatchedRoute[] | null {\n const hasChildren = Boolean(route.children?.length);\n\n // For parent routes (with children), we need to match as a prefix\n // For leaf routes (no children), we need an exact match\n const { matched, params, consumedPathname } = matchPath(\n route.path,\n pathname,\n !hasChildren,\n );\n\n if (!matched) {\n return null;\n }\n\n const result: MatchedRoute = {\n route,\n params,\n pathname: consumedPathname,\n };\n\n // If this route has children, try to match them\n if (hasChildren) {\n // Calculate remaining pathname, ensuring it starts with /\n let remainingPathname = pathname.slice(consumedPathname.length);\n if (!remainingPathname.startsWith(\"/\")) {\n remainingPathname = \"/\" + remainingPathname;\n }\n if (remainingPathname === \"\") {\n remainingPathname = \"/\";\n }\n\n for (const child of route.children!) {\n const childMatch = matchRoute(child, remainingPathname);\n if (childMatch) {\n // Merge params from parent into children\n return [\n result,\n ...childMatch.map((m) => ({\n ...m,\n params: { ...params, ...m.params },\n })),\n ];\n }\n }\n\n // If no children matched but this route has a component, it's still a valid match\n if (route.component) {\n return [result];\n }\n\n return null;\n }\n\n return [result];\n}\n\n/**\n * Match a path pattern against a pathname.\n */\nfunction matchPath(\n pattern: string,\n pathname: string,\n exact: boolean,\n): {\n matched: boolean;\n params: Record<string, string>;\n consumedPathname: string;\n} {\n // Normalize pattern\n const normalizedPattern = pattern.startsWith(\"/\") ? pattern : `/${pattern}`;\n\n // Build URLPattern\n let urlPatternPath: string;\n if (exact) {\n urlPatternPath = normalizedPattern;\n } else if (normalizedPattern === \"/\") {\n // Special case: root path as prefix matches anything\n urlPatternPath = \"/*\";\n } else {\n // For other prefix matches, add optional wildcard suffix\n urlPatternPath = `${normalizedPattern}{/*}?`;\n }\n\n const urlPattern = new URLPattern({ pathname: urlPatternPath });\n\n const match = urlPattern.exec({ pathname });\n if (!match) {\n return { matched: false, params: {}, consumedPathname: \"\" };\n }\n\n // Extract params (excluding the wildcard group \"0\")\n const params: Record<string, string> = {};\n for (const [key, value] of Object.entries(match.pathname.groups)) {\n if (value !== undefined && key !== \"0\") {\n params[key] = value;\n }\n }\n\n // Calculate consumed pathname\n let consumedPathname: string;\n if (exact) {\n consumedPathname = pathname;\n } else if (normalizedPattern === \"/\") {\n // Root pattern consumes just \"/\"\n consumedPathname = \"/\";\n } else {\n // For prefix matches, calculate based on pattern segments\n const patternSegments = normalizedPattern.split(\"/\").filter(Boolean);\n const pathnameSegments = pathname.split(\"/\").filter(Boolean);\n consumedPathname =\n \"/\" + pathnameSegments.slice(0, patternSegments.length).join(\"/\");\n }\n\n return { matched: true, params, consumedPathname };\n}\n","import type { LoaderArgs } from \"../route.js\";\nimport type {\n MatchedRoute,\n MatchedRouteWithData,\n InternalRouteDefinition,\n} from \"../types.js\";\n\n/**\n * Cache for loader results.\n * Key format: `${entryId}:${matchIndex}`\n */\nconst loaderCache = new Map<string, unknown>();\n\n/**\n * Get or create a loader result from cache.\n * If the result is not cached, executes the loader and caches the result.\n */\nfunction getOrCreateLoaderResult(\n entryId: string,\n matchIndex: number,\n route: InternalRouteDefinition,\n args: LoaderArgs,\n): unknown | undefined {\n if (!route.loader) {\n return undefined;\n }\n\n const cacheKey = `${entryId}:${matchIndex}`;\n\n if (!loaderCache.has(cacheKey)) {\n loaderCache.set(cacheKey, route.loader(args));\n }\n\n return loaderCache.get(cacheKey);\n}\n\n/**\n * Create a Request object for loader args.\n */\nexport function createLoaderRequest(url: URL): Request {\n return new Request(url.href, {\n method: \"GET\",\n });\n}\n\n/**\n * Execute loaders for matched routes and return routes with data.\n * Results are cached by navigation entry id to prevent duplicate execution.\n */\nexport function executeLoaders(\n matchedRoutes: MatchedRoute[],\n entryId: string,\n request: Request,\n signal: AbortSignal,\n): MatchedRouteWithData[] {\n return matchedRoutes.map((match, index) => {\n const { route, params } = match;\n const args: LoaderArgs = { params, request, signal };\n const data = getOrCreateLoaderResult(entryId, index, route, args);\n\n return { ...match, data };\n });\n}\n\n/**\n * Clear the loader cache.\n * Mainly used for testing.\n */\nexport function clearLoaderCache(): void {\n loaderCache.clear();\n}\n\n/**\n * Clear loader cache entries for a specific navigation entry.\n * Called when a NavigationHistoryEntry is disposed (removed from history stack).\n */\nexport function clearLoaderCacheForEntry(entryId: string): void {\n const prefix = `${entryId}:`;\n for (const key of loaderCache.keys()) {\n if (key.startsWith(prefix)) {\n loaderCache.delete(key);\n }\n }\n}\n","import type { RouterAdapter, LocationEntry } from \"./RouterAdapter.js\";\nimport type {\n InternalRouteDefinition,\n NavigateOptions,\n OnNavigateCallback,\n} from \"../types.js\";\nimport { matchRoutes } from \"./matchRoutes.js\";\nimport {\n executeLoaders,\n createLoaderRequest,\n clearLoaderCacheForEntry,\n} from \"./loaderCache.js\";\n\n/**\n * Fallback AbortController for data loading initialized outside of a navigation event.\n * Aborted when the next navigation occurs.\n *\n * To save resources, this controller is created only when needed.\n */\nlet idleController: AbortController | null = null;\n\n/**\n * Reset navigation state. Used for testing.\n */\nexport function resetNavigationState(): void {\n idleController?.abort();\n idleController = null;\n}\n\n/**\n * Adapter that uses the Navigation API for full SPA functionality.\n */\nexport class NavigationAPIAdapter implements RouterAdapter {\n // Cache the snapshot to ensure referential stability for useSyncExternalStore\n #cachedSnapshot: LocationEntry | null = null;\n #cachedEntryId: string | null = null;\n // Ephemeral info from the current navigation event (not persisted in history)\n #currentNavigationInfo: unknown = undefined;\n\n getSnapshot(): LocationEntry | null {\n const entry = navigation.currentEntry;\n if (!entry?.url) {\n return null;\n }\n\n // Return cached snapshot if entry hasn't changed\n if (this.#cachedEntryId === entry.id && this.#cachedSnapshot) {\n return this.#cachedSnapshot;\n }\n\n // Create new snapshot and cache it\n this.#cachedEntryId = entry.id;\n this.#cachedSnapshot = {\n url: new URL(entry.url),\n key: entry.id,\n state: entry.getState(),\n info: this.#currentNavigationInfo,\n };\n return this.#cachedSnapshot;\n }\n\n getServerSnapshot(): LocationEntry | null {\n return null;\n }\n\n subscribe(callback: () => void): () => void {\n const controller = new AbortController();\n navigation.addEventListener(\"currententrychange\", callback, {\n signal: controller.signal,\n });\n\n // Subscribe to dispose events on all existing entries\n this.#subscribeToDisposeEvents(controller.signal);\n\n // When current entry changes, subscribe to any new entries' dispose events\n navigation.addEventListener(\n \"currententrychange\",\n () => this.#subscribeToDisposeEvents(controller.signal),\n { signal: controller.signal },\n );\n\n return () => {\n controller.abort();\n };\n }\n\n /**\n * Track which entries we've subscribed to dispose events for.\n */\n #subscribedEntryIds = new Set<string>();\n\n /**\n * Subscribe to dispose events on all navigation entries.\n * When an entry is disposed, its cached loader results are cleared.\n */\n #subscribeToDisposeEvents(signal: AbortSignal): void {\n for (const entry of navigation.entries()) {\n if (this.#subscribedEntryIds.has(entry.id)) {\n continue;\n }\n this.#subscribedEntryIds.add(entry.id);\n\n const entryId = entry.id;\n entry.addEventListener(\n \"dispose\",\n () => {\n clearLoaderCacheForEntry(entryId);\n this.#subscribedEntryIds.delete(entryId);\n },\n { signal },\n );\n }\n }\n\n navigate(to: string, options?: NavigateOptions): void {\n navigation.navigate(to, {\n history: options?.replace ? \"replace\" : \"push\",\n state: options?.state,\n info: options?.info,\n });\n }\n\n async navigateAsync(to: string, options?: NavigateOptions): Promise<void> {\n const result = navigation.navigate(to, {\n history: options?.replace ? \"replace\" : \"push\",\n state: options?.state,\n info: options?.info,\n });\n await result.finished;\n }\n\n setupInterception(\n routes: InternalRouteDefinition[],\n onNavigate?: OnNavigateCallback,\n checkBlockers?: () => boolean,\n ): (() => void) | undefined {\n const handleNavigate = (event: NavigateEvent) => {\n // Capture ephemeral info from the navigate event\n // This info is only available during this navigation and resets on the next one\n this.#currentNavigationInfo = event.info;\n // Invalidate cached snapshot to pick up new info\n this.#cachedSnapshot = null;\n\n // Check blockers first - if any blocker returns true, prevent navigation\n if (checkBlockers?.()) {\n event.preventDefault();\n return;\n }\n\n // Only intercept same-origin navigations\n if (!event.canIntercept) {\n onNavigate?.(event, []);\n return;\n }\n\n // Check if the URL matches any of our routes\n const url = new URL(event.destination.url);\n const matched = matchRoutes(routes, url.pathname);\n\n // Call onNavigate callback if provided (regardless of route match)\n if (onNavigate) {\n onNavigate(event, matched);\n if (event.defaultPrevented) {\n return; // Do not intercept, allow browser default\n }\n }\n\n // hash change navigations are not intercepted.\n // Also do not intercept if it is a download request.\n if (event.hashChange || event.downloadRequest !== null) {\n return;\n }\n\n if (!matched) {\n return;\n }\n\n // Route match, so intercept\n\n // Abort initial load's loaders if this is the first navigation\n if (idleController) {\n idleController.abort();\n idleController = null;\n }\n\n event.intercept({\n handler: async () => {\n const request = createLoaderRequest(url);\n\n // Note: in response to `currententrychange` event, <Router> should already\n // have dispatched data loaders and the results should be cached.\n // Here we run executeLoader to retrieve cached results.\n const currentEntry = navigation.currentEntry;\n if (!currentEntry) {\n throw new Error(\n \"Navigation currentEntry is null during navigation interception\",\n );\n }\n\n const results = executeLoaders(\n matched,\n currentEntry.id,\n request,\n event.signal,\n );\n\n // Delay navigation until async loaders complete\n await Promise.all(results.map((r) => r.data));\n },\n });\n };\n\n const controller = new AbortController();\n navigation.addEventListener(\"navigate\", handleNavigate, {\n signal: controller.signal,\n });\n return () => {\n controller.abort();\n };\n }\n\n getIdleAbortSignal(): AbortSignal {\n idleController ??= new AbortController();\n return idleController.signal;\n }\n\n updateCurrentEntryState(state: unknown): void {\n // Invalidate cached snapshot BEFORE updating, so the subscriber gets fresh state\n this.#cachedSnapshot = null;\n navigation.updateCurrentEntry({ state });\n // Note: updateCurrentEntry fires currententrychange, so subscribers are notified automatically\n }\n}\n","import type { RouterAdapter, LocationEntry } from \"./RouterAdapter.js\";\nimport type {\n InternalRouteDefinition,\n NavigateOptions,\n OnNavigateCallback,\n} from \"../types.js\";\n\n/**\n * Static adapter for fallback mode when Navigation API is unavailable.\n * Provides read-only location access with no SPA navigation capabilities.\n * Links will cause full page loads (MPA behavior).\n */\nexport class StaticAdapter implements RouterAdapter {\n #cachedSnapshot: LocationEntry | null = null;\n #idleController: AbortController | null = null;\n\n getSnapshot(): LocationEntry | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n\n // Cache the snapshot - it never changes in static mode\n if (!this.#cachedSnapshot) {\n this.#cachedSnapshot = {\n url: new URL(window.location.href),\n key: \"__static__\",\n state: undefined,\n info: undefined,\n };\n }\n return this.#cachedSnapshot;\n }\n\n getServerSnapshot(): LocationEntry | null {\n return null;\n }\n\n subscribe(_callback: () => void): () => void {\n // Static mode never fires location change events\n return () => {};\n }\n\n navigate(to: string, _options?: NavigateOptions): void {\n console.warn(\n \"FUNSTACK Router: navigate() called in static fallback mode. \" +\n \"Navigation API is not available in this browser. \" +\n \"Links will cause full page loads.\",\n );\n // Note: We intentionally do NOT do window.location.href = to\n // as that would mask bugs where developers expect SPA behavior.\n // If needed in the future, we could add a \"static-reload\" mode.\n }\n\n async navigateAsync(to: string, options?: NavigateOptions): Promise<void> {\n this.navigate(to, options);\n }\n\n setupInterception(\n _routes: InternalRouteDefinition[],\n _onNavigate?: OnNavigateCallback,\n _checkBlockers?: () => boolean,\n ): (() => void) | undefined {\n // No interception in static mode - links cause full page loads\n return undefined;\n }\n\n getIdleAbortSignal(): AbortSignal {\n this.#idleController ??= new AbortController();\n return this.#idleController.signal;\n }\n\n updateCurrentEntryState(_state: unknown): void {\n // No-op in static mode - state updates require Navigation API\n }\n}\n","import type { RouterAdapter, LocationEntry } from \"./RouterAdapter.js\";\nimport type {\n InternalRouteDefinition,\n NavigateOptions,\n OnNavigateCallback,\n} from \"../types.js\";\n\n/**\n * Null adapter for when Navigation API is unavailable and no fallback is configured.\n * All methods are no-ops that return safe default values.\n */\nexport class NullAdapter implements RouterAdapter {\n #idleController: AbortController | null = null;\n\n getSnapshot(): LocationEntry | null {\n return null;\n }\n\n getServerSnapshot(): LocationEntry | null {\n return null;\n }\n\n subscribe(_callback: () => void): () => void {\n return () => {};\n }\n\n navigate(_to: string, _options?: NavigateOptions): void {\n console.warn(\n \"FUNSTACK Router: navigate() called but no adapter is available. \" +\n \"Navigation API is not available in this browser and no fallback mode is configured.\",\n );\n }\n\n async navigateAsync(to: string, options?: NavigateOptions): Promise<void> {\n this.navigate(to, options);\n }\n\n setupInterception(\n _routes: InternalRouteDefinition[],\n _onNavigate?: OnNavigateCallback,\n _checkBlockers?: () => boolean,\n ): (() => void) | undefined {\n return undefined;\n }\n\n getIdleAbortSignal(): AbortSignal {\n this.#idleController ??= new AbortController();\n return this.#idleController.signal;\n }\n\n updateCurrentEntryState(_state: unknown): void {\n // No-op: NullAdapter doesn't support state updates\n }\n}\n","import type { RouterAdapter } from \"./RouterAdapter.js\";\nimport { NavigationAPIAdapter } from \"./NavigationAPIAdapter.js\";\nimport { StaticAdapter } from \"./StaticAdapter.js\";\nimport { NullAdapter } from \"./NullAdapter.js\";\nimport type { FallbackMode } from \"../types.js\";\n\n/**\n * Check if Navigation API is available.\n */\nfunction hasNavigation(): boolean {\n return typeof window !== \"undefined\" && \"navigation\" in window;\n}\n\n/**\n * Create the appropriate router adapter based on browser capabilities\n * and the specified fallback mode.\n *\n * @param fallback - The fallback mode to use when Navigation API is unavailable\n * @returns A RouterAdapter instance\n */\nexport function createAdapter(fallback: FallbackMode): RouterAdapter {\n // Try Navigation API first\n if (hasNavigation()) {\n return new NavigationAPIAdapter();\n }\n\n // Fall back to static mode if enabled\n if (fallback === \"static\") {\n return new StaticAdapter();\n }\n\n // No adapter available (fallback=\"none\" or default)\n return new NullAdapter();\n}\n","import {\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport { RouterContext } from \"./context/RouterContext.js\";\nimport { RouteContext } from \"./context/RouteContext.js\";\nimport {\n BlockerContext,\n createBlockerRegistry,\n} from \"./context/BlockerContext.js\";\nimport {\n type NavigateOptions,\n type MatchedRouteWithData,\n type OnNavigateCallback,\n type FallbackMode,\n type InternalRouteState,\n internalRoutes,\n} from \"./types.js\";\nimport { matchRoutes } from \"./core/matchRoutes.js\";\nimport { createAdapter } from \"./core/createAdapter.js\";\nimport { executeLoaders, createLoaderRequest } from \"./core/loaderCache.js\";\nimport type { RouteDefinition } from \"./route.js\";\n\nexport type RouterProps = {\n routes: RouteDefinition[];\n /**\n * Callback invoked before navigation is intercepted.\n * Call `event.preventDefault()` to prevent the router from handling this navigation.\n *\n * @param event - The NavigateEvent from the Navigation API\n * @param matched - Array of matched routes, or null if no routes matched\n */\n onNavigate?: OnNavigateCallback;\n /**\n * Fallback mode when Navigation API is unavailable.\n *\n * - `\"none\"` (default): Render nothing when Navigation API is unavailable\n * - `\"static\"`: Render matched routes without navigation capabilities (MPA behavior)\n */\n fallback?: FallbackMode;\n};\n\nexport function Router({\n routes: inputRoutes,\n onNavigate,\n fallback = \"none\",\n}: RouterProps): ReactNode {\n const routes = internalRoutes(inputRoutes);\n\n // Create adapter once based on browser capabilities and fallback setting\n const adapter = useMemo(() => createAdapter(fallback), [fallback]);\n\n // Create blocker registry once\n const [blockerRegistry] = useState(() => createBlockerRegistry());\n\n // Subscribe to location changes via adapter\n const locationEntry = useSyncExternalStore(\n useCallback((callback) => adapter.subscribe(callback), [adapter]),\n () => adapter.getSnapshot(),\n () => adapter.getServerSnapshot(),\n );\n\n // Set up navigation interception via adapter\n useEffect(() => {\n return adapter.setupInterception(\n routes,\n onNavigate,\n blockerRegistry.checkAll,\n );\n }, [adapter, routes, onNavigate, blockerRegistry]);\n\n // Navigate function from adapter\n const navigate = useCallback(\n (to: string, options?: NavigateOptions) => {\n adapter.navigate(to, options);\n },\n [adapter],\n );\n\n // Navigate function that returns a Promise\n const navigateAsync = useCallback(\n (to: string, options?: NavigateOptions) => {\n return adapter.navigateAsync(to, options);\n },\n [adapter],\n );\n\n // Update current entry's state without navigation\n const updateCurrentEntryState = useCallback(\n (state: unknown) => {\n adapter.updateCurrentEntryState(state);\n },\n [adapter],\n );\n\n return useMemo(() => {\n if (locationEntry === null) {\n // This happens either when Navigation API is unavailable (and no fallback),\n // or the current document is not fully active.\n return null;\n }\n\n const { url, key } = locationEntry;\n\n // Match current URL against routes and execute loaders\n const matchedRoutesWithData = (() => {\n const matched = matchRoutes(routes, url.pathname);\n if (!matched) return null;\n\n // Execute loaders (results are cached by location entry key)\n const request = createLoaderRequest(url);\n const signal = adapter.getIdleAbortSignal();\n return executeLoaders(matched, key, request, signal);\n })();\n\n const routerContextValue = {\n locationEntry,\n url,\n navigate,\n navigateAsync,\n updateCurrentEntryState,\n };\n\n const blockerContextValue = { registry: blockerRegistry };\n\n return (\n <BlockerContext.Provider value={blockerContextValue}>\n <RouterContext.Provider value={routerContextValue}>\n {matchedRoutesWithData ? (\n <RouteRenderer matchedRoutes={matchedRoutesWithData} index={0} />\n ) : null}\n </RouterContext.Provider>\n </BlockerContext.Provider>\n );\n }, [\n navigate,\n navigateAsync,\n updateCurrentEntryState,\n locationEntry,\n routes,\n adapter,\n blockerRegistry,\n ]);\n}\n\ntype RouteRendererProps = {\n matchedRoutes: MatchedRouteWithData[];\n index: number;\n};\n\n/**\n * Recursively render matched routes with proper context.\n */\nfunction RouteRenderer({\n matchedRoutes,\n index,\n}: RouteRendererProps): ReactNode {\n const match = matchedRoutes[index];\n if (!match) return null;\n\n const { route, params, pathname, data } = match;\n const Component = route.component;\n\n const routerContext = useContext(RouterContext);\n if (!routerContext) {\n throw new Error(\"RouteRenderer must be used within RouterContext\");\n }\n const { locationEntry, url, navigateAsync, updateCurrentEntryState } =\n routerContext;\n\n // Extract this route's state from internal structure\n const internalState = locationEntry.state as InternalRouteState | undefined;\n const routeState = internalState?.__routeStates?.[index];\n\n // Create stable setStateSync callback for this route's slice (synchronous via updateCurrentEntry)\n const setStateSync = useCallback(\n (stateOrUpdater: unknown | ((prev: unknown) => unknown)) => {\n const currentStates =\n (locationEntry.state as InternalRouteState | undefined)\n ?.__routeStates ?? [];\n const currentRouteState = currentStates[index];\n\n const newState =\n typeof stateOrUpdater === \"function\"\n ? (stateOrUpdater as (prev: unknown) => unknown)(currentRouteState)\n : stateOrUpdater;\n\n const newStates = [...currentStates];\n newStates[index] = newState;\n updateCurrentEntryState({ __routeStates: newStates });\n },\n [locationEntry.state, index, updateCurrentEntryState],\n );\n\n // Create stable setState callback for this route's slice (async via replace navigation)\n const setState = useCallback(\n async (\n stateOrUpdater: unknown | ((prev: unknown) => unknown),\n ): Promise<void> => {\n const currentStates =\n (locationEntry.state as InternalRouteState | undefined)\n ?.__routeStates ?? [];\n const currentRouteState = currentStates[index];\n\n const newState =\n typeof stateOrUpdater === \"function\"\n ? (stateOrUpdater as (prev: unknown) => unknown)(currentRouteState)\n : stateOrUpdater;\n\n const newStates = [...currentStates];\n newStates[index] = newState;\n\n await navigateAsync(url.href, {\n replace: true,\n state: { __routeStates: newStates },\n });\n },\n [locationEntry.state, index, url, navigateAsync],\n );\n\n // Create stable resetState callback\n const resetState = useCallback(() => {\n const currentStates =\n (locationEntry.state as InternalRouteState | undefined)?.__routeStates ??\n [];\n const newStates = [...currentStates];\n newStates[index] = undefined;\n updateCurrentEntryState({ __routeStates: newStates });\n }, [locationEntry.state, index, updateCurrentEntryState]);\n\n // Create outlet for child routes\n const outlet =\n index < matchedRoutes.length - 1 ? (\n <RouteRenderer matchedRoutes={matchedRoutes} index={index + 1} />\n ) : null;\n\n const routeContextValue = useMemo(\n () => ({ params, matchedPath: pathname, outlet }),\n [params, pathname, outlet],\n );\n\n // Render component with or without data prop based on loader presence\n // Always pass params, state, setState, resetState, and info props to components\n const renderComponent = () => {\n if (!Component) return outlet;\n\n const stateProps = {\n state: routeState,\n setState,\n setStateSync,\n resetState,\n };\n\n // Ephemeral info from the current navigation\n const { info } = locationEntry;\n\n // When loader exists, data is defined and component expects data prop\n // When loader doesn't exist, data is undefined and component doesn't expect data prop\n // TypeScript can't narrow this union, so we use runtime check with type assertion\n if (route.loader) {\n const ComponentWithData = Component as React.ComponentType<{\n data: unknown;\n params: Record<string, string>;\n state: unknown;\n setState: (s: unknown | ((prev: unknown) => unknown)) => Promise<void>;\n setStateSync: (s: unknown | ((prev: unknown) => unknown)) => void;\n resetState: () => void;\n info: unknown;\n }>;\n return (\n <ComponentWithData\n data={data}\n params={params}\n {...stateProps}\n info={info}\n />\n );\n }\n const ComponentWithoutData = Component as React.ComponentType<{\n params: Record<string, string>;\n state: unknown;\n setState: (s: unknown | ((prev: unknown) => unknown)) => Promise<void>;\n setStateSync: (s: unknown | ((prev: unknown) => unknown)) => void;\n resetState: () => void;\n info: unknown;\n }>;\n return <ComponentWithoutData params={params} {...stateProps} info={info} />;\n };\n\n return (\n <RouteContext.Provider value={routeContextValue}>\n {renderComponent()}\n </RouteContext.Provider>\n );\n}\n","import { type ReactNode, useContext } from \"react\";\nimport { RouteContext } from \"./context/RouteContext.js\";\n\n/**\n * Renders the matched child route.\n * Used in layout components to specify where child routes should render.\n */\nexport function Outlet(): ReactNode {\n const routeContext = useContext(RouteContext);\n\n if (!routeContext) {\n return null;\n }\n\n return routeContext.outlet;\n}\n","import { useContext } from \"react\";\nimport { RouterContext } from \"../context/RouterContext.js\";\nimport type { NavigateOptions } from \"../types.js\";\n\n/**\n * Returns a function for programmatic navigation.\n */\nexport function useNavigate(): (to: string, options?: NavigateOptions) => void {\n const context = useContext(RouterContext);\n\n if (!context) {\n throw new Error(\"useNavigate must be used within a Router\");\n }\n\n return context.navigate;\n}\n","import { useContext, useMemo } from \"react\";\nimport { RouterContext } from \"../context/RouterContext.js\";\nimport type { Location } from \"../types.js\";\n\n/**\n * Returns the current location object.\n */\nexport function useLocation(): Location {\n const context = useContext(RouterContext);\n\n if (!context) {\n throw new Error(\"useLocation must be used within a Router\");\n }\n\n const { url } = context;\n\n return useMemo(() => {\n return {\n pathname: url.pathname,\n search: url.search,\n hash: url.hash,\n };\n }, [url]);\n}\n","import { useCallback, useContext, useMemo } from \"react\";\nimport { RouterContext } from \"../context/RouterContext.js\";\n\ntype SetSearchParams = (\n params:\n | URLSearchParams\n | Record<string, string>\n | ((prev: URLSearchParams) => URLSearchParams | Record<string, string>),\n) => void;\n\n/**\n * Returns and allows manipulation of URL search parameters.\n */\nexport function useSearchParams(): [URLSearchParams, SetSearchParams] {\n const context = useContext(RouterContext);\n\n if (!context) {\n throw new Error(\"useSearchParams must be used within a Router\");\n }\n\n const searchParams = context.url.searchParams;\n\n const setSearchParams = useCallback<SetSearchParams>(\n (params) => {\n const url = new URL(context.url);\n\n let newParams: URLSearchParams;\n if (typeof params === \"function\") {\n const result = params(new URLSearchParams(url.search));\n newParams =\n result instanceof URLSearchParams\n ? result\n : new URLSearchParams(result);\n } else if (params instanceof URLSearchParams) {\n newParams = params;\n } else {\n newParams = new URLSearchParams(params);\n }\n\n url.search = newParams.toString();\n context.navigate(url.pathname + url.search + url.hash, { replace: true });\n },\n [context],\n );\n\n return [searchParams, setSearchParams];\n}\n","import { useContext, useEffect, useId } from \"react\";\nimport { BlockerContext } from \"../context/BlockerContext.js\";\n\nexport type UseBlockerOptions = {\n /**\n * Function that returns true if navigation should be blocked.\n * Can call `confirm()` inside to show a confirmation dialog.\n */\n shouldBlock: () => boolean;\n};\n\n/**\n * Hook to block navigation away from the current route.\n *\n * This is useful for scenarios like unsaved form data, ongoing file uploads,\n * or any state that would be lost on navigation.\n *\n * @example\n * ```tsx\n * function EditForm() {\n * const [isDirty, setIsDirty] = useState(false);\n *\n * useBlocker({\n * shouldBlock: () => {\n * if (isDirty) {\n * return !confirm(\"You have unsaved changes. Leave anyway?\");\n * }\n * return false;\n * },\n * });\n *\n * return <form>...</form>;\n * }\n * ```\n *\n * Note: This hook only handles SPA navigations (links, programmatic navigation).\n * For hard navigations (tab close, refresh), handle `beforeunload` separately.\n */\nexport function useBlocker(options: UseBlockerOptions): void {\n const context = useContext(BlockerContext);\n\n if (!context) {\n throw new Error(\"useBlocker must be used within a Router\");\n }\n\n const { shouldBlock } = options;\n const blockerId = useId();\n const { registry } = context;\n\n // Register blocker on mount, unregister on unmount\n // Re-registers when shouldBlock function changes\n useEffect(() => {\n return registry.register(blockerId, shouldBlock);\n }, [blockerId, shouldBlock, registry]);\n}\n","import type { ComponentType } from \"react\";\n\nconst routeDefinitionSymbol = Symbol();\n\n/**\n * Extracts parameter names from a path pattern.\n * E.g., \"/users/:id/posts/:postId\" -> \"id\" | \"postId\"\n */\ntype ExtractParams<T extends string> =\n T extends `${string}:${infer Param}/${infer Rest}`\n ? Param | ExtractParams<`/${Rest}`>\n : T extends `${string}:${infer Param}`\n ? Param\n : never;\n\n/**\n * Creates a params object type from a path pattern.\n * E.g., \"/users/:id\" -> { id: string }\n */\nexport type PathParams<T extends string> = [ExtractParams<T>] extends [never]\n ? Record<string, never>\n : { [K in ExtractParams<T>]: string };\n\n/**\n * Arguments passed to loader functions.\n */\nexport type LoaderArgs = {\n /** Extracted path parameters */\n params: Record<string, string>;\n /** Request object with URL and headers */\n request: Request;\n /** AbortSignal for cancellation on navigation */\n signal: AbortSignal;\n};\n\n/**\n * Props for route components without loader.\n * Includes navigation state management props.\n */\nexport type RouteComponentProps<\n TParams extends Record<string, string>,\n TState = undefined,\n> = {\n /** Extracted path parameters */\n params: TParams;\n /** Current navigation state for this route (undefined on first visit) */\n state: TState | undefined;\n /** Update navigation state for this route asynchronously via replace navigation */\n setState: (\n state: TState | ((prev: TState | undefined) => TState),\n ) => Promise<void>;\n /** Update navigation state for this route synchronously via updateCurrentEntry */\n setStateSync: (\n state: TState | ((prev: TState | undefined) => TState),\n ) => void;\n /** Reset navigation state to undefined */\n resetState: () => void;\n /** Ephemeral navigation info (only available during navigation, not persisted) */\n info: unknown;\n};\n\n/**\n * Props for route components with loader.\n * Includes data from loader and navigation state management props.\n */\nexport type RouteComponentPropsWithData<\n TParams extends Record<string, string>,\n TData,\n TState = undefined,\n> = RouteComponentProps<TParams, TState> & {\n /** Data returned from the loader */\n data: TData;\n};\n\n/**\n * Route definition created by the `route` helper function.\n */\nexport interface OpaqueRouteDefinition {\n [routeDefinitionSymbol]: never;\n path: string;\n children?: RouteDefinition[];\n}\n\n/**\n * Any route definition defined by user.\n */\nexport type RouteDefinition =\n | OpaqueRouteDefinition\n | {\n path: string;\n component?: ComponentType<object>;\n children?: RouteDefinition[];\n };\n\n/**\n * Route definition with loader - infers TData from loader return type.\n * TPath is used to infer params type from the path pattern.\n * TState is the type of navigation state for this route.\n */\ntype RouteWithLoader<TPath extends string, TData, TState> = {\n path: TPath;\n loader: (args: LoaderArgs) => TData;\n component: ComponentType<\n RouteComponentPropsWithData<PathParams<TPath>, TData, TState>\n >;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n children?: RouteDefinition[];\n};\n\n/**\n * Route definition without loader.\n * TPath is used to infer params type from the path pattern.\n * TState is the type of navigation state for this route.\n */\ntype RouteWithoutLoader<TPath extends string, TState> = {\n path: TPath;\n component?: ComponentType<RouteComponentProps<PathParams<TPath>, TState>>;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n children?: RouteDefinition[];\n};\n\n/**\n * Helper function for creating type-safe route definitions.\n *\n * When a loader is provided, TypeScript infers the return type and ensures\n * the component accepts a `data` prop of that type. Components always receive\n * a `params` prop with types inferred from the path pattern.\n *\n * For routes with navigation state, use `routeState<TState>()({ ... })` instead.\n *\n * @example\n * ```typescript\n * // Route with async loader\n * route({\n * path: \"users/:userId\",\n * loader: async ({ params, signal }) => {\n * const res = await fetch(`/api/users/${params.userId}`, { signal });\n * return res.json() as Promise<User>;\n * },\n * component: UserDetail, // Must accept { data: Promise<User>, params: { userId: string }, state, setState, resetState }\n * });\n *\n * // Route without loader\n * route({\n * path: \"about\",\n * component: AboutPage, // Must accept { params: {}, state, setState, resetState }\n * });\n * ```\n */\n// Overload with loader\nexport function route<TPath extends string, TData>(\n definition: RouteWithLoader<TPath, TData, undefined>,\n): OpaqueRouteDefinition;\n// Overload without loader\nexport function route<TPath extends string>(\n definition: RouteWithoutLoader<TPath, undefined>,\n): OpaqueRouteDefinition;\n// Implementation\nexport function route<TPath extends string, TData>(\n definition:\n | RouteWithLoader<TPath, TData, undefined>\n | RouteWithoutLoader<TPath, undefined>,\n): OpaqueRouteDefinition {\n return definition as unknown as OpaqueRouteDefinition;\n}\n\n/**\n * Helper function for creating type-safe route definitions with navigation state.\n *\n * Use this curried function when your route component needs to manage navigation state.\n * The state is tied to the navigation history entry and persists across back/forward navigation.\n *\n * @example\n * ```typescript\n * // Route with navigation state\n * type MyState = { scrollPosition: number };\n * routeState<MyState>()({\n * path: \"users/:userId\",\n * component: UserPage, // Receives { params, state, setState, resetState }\n * });\n *\n * // Route with both loader and navigation state\n * type FilterState = { filter: string };\n * routeState<FilterState>()({\n * path: \"products\",\n * loader: async () => fetchProducts(),\n * component: ProductList, // Receives { data, params, state, setState, resetState }\n * });\n * ```\n */\nexport function routeState<TState>(): {\n <TPath extends string, TData>(\n definition: RouteWithLoader<TPath, TData, TState>,\n ): OpaqueRouteDefinition;\n <TPath extends string>(\n definition: RouteWithoutLoader<TPath, TState>,\n ): OpaqueRouteDefinition;\n} {\n return function <TPath extends string, TData>(\n definition:\n | RouteWithLoader<TPath, TData, TState>\n | RouteWithoutLoader<TPath, TState>,\n ): OpaqueRouteDefinition {\n return definition as unknown as OpaqueRouteDefinition;\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funstack/router",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A modern React router based on the Navigation API",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,12 +31,12 @@
31
31
  "@testing-library/jest-dom": "^6.9.1",
32
32
  "@testing-library/react": "^16.3.1",
33
33
  "@types/react": "^19.0.0",
34
- "jsdom": "^27.3.0",
34
+ "jsdom": "^27.4.0",
35
35
  "react": "^19.0.0",
36
- "tsdown": "^0.18.0",
36
+ "tsdown": "^0.18.2",
37
37
  "typescript": "^5.7.0",
38
38
  "urlpattern-polyfill": "^10.1.0",
39
- "vitest": "^4.0.15"
39
+ "vitest": "^4.0.16"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsdown",