@fictjs/router 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,19 +1,34 @@
1
1
  // src/components.tsx
2
2
  import {
3
3
  createEffect,
4
- onCleanup,
5
4
  createMemo,
6
- batch,
7
- untrack,
8
- startTransition,
9
5
  Fragment,
10
6
  Suspense,
11
7
  ErrorBoundary
12
8
  } from "@fictjs/runtime";
13
- import { createSignal } from "@fictjs/runtime/advanced";
9
+ import { createSignal as createSignal3 } from "@fictjs/runtime/advanced";
10
+
11
+ // src/accessor-utils.ts
12
+ function wrapAccessor(fn) {
13
+ const wrapped = ((...args) => {
14
+ if (args.length === 0) return wrapped;
15
+ return fn(...args);
16
+ });
17
+ return wrapped;
18
+ }
19
+ function wrapValue(value) {
20
+ const wrapped = (() => value);
21
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
22
+ const primitive = value;
23
+ wrapped.toString = () => String(primitive);
24
+ wrapped.valueOf = () => primitive;
25
+ }
26
+ return wrapped;
27
+ }
14
28
 
15
29
  // src/context.ts
16
- import { createContext, useContext } from "@fictjs/runtime";
30
+ import { batch, createContext, useContext } from "@fictjs/runtime";
31
+ import { createSignal } from "@fictjs/runtime/advanced";
17
32
 
18
33
  // src/utils.ts
19
34
  function normalizePath(path) {
@@ -342,23 +357,76 @@ function isBrowser() {
342
357
  }
343
358
 
344
359
  // src/context.ts
345
- var defaultRouterContext = {
346
- location: () => ({
347
- pathname: "/",
348
- search: "",
349
- hash: "",
350
- state: null,
351
- key: "default"
352
- }),
353
- params: () => ({}),
354
- matches: () => [],
355
- navigate: () => {
360
+ function readAccessor(value) {
361
+ return typeof value === "function" ? value() : value;
362
+ }
363
+ var activeRouter = createSignal(null);
364
+ var activeRouterStack = [];
365
+ function pushActiveRouter(router) {
366
+ activeRouterStack.push(router);
367
+ batch(() => {
368
+ activeRouter(router);
369
+ });
370
+ }
371
+ function popActiveRouter(router) {
372
+ const index = activeRouterStack.lastIndexOf(router);
373
+ if (index >= 0) {
374
+ activeRouterStack.splice(index, 1);
375
+ }
376
+ batch(() => {
377
+ activeRouter(activeRouterStack[activeRouterStack.length - 1] ?? null);
378
+ });
379
+ }
380
+ var defaultLocation = {
381
+ pathname: "/",
382
+ search: "",
383
+ hash: "",
384
+ state: null,
385
+ key: "default"
386
+ };
387
+ var defaultNavigate = wrapAccessor(((toOrDelta) => {
388
+ const router = activeRouter();
389
+ if (!router) {
356
390
  console.warn("[fict-router] No router found. Wrap your app in a <Router>");
391
+ return;
392
+ }
393
+ const navigate = readAccessor(router.navigate);
394
+ if (typeof toOrDelta === "number") {
395
+ return navigate(toOrDelta);
396
+ }
397
+ return navigate(toOrDelta);
398
+ }));
399
+ var defaultResolvePath = wrapAccessor((to) => {
400
+ const router = activeRouter();
401
+ if (router) {
402
+ return readAccessor(router.resolvePath)(to);
403
+ }
404
+ return typeof to === "string" ? to : to.pathname || "/";
405
+ });
406
+ var defaultRouterContext = {
407
+ location: () => {
408
+ const router = activeRouter();
409
+ return router ? readAccessor(router.location) : defaultLocation;
410
+ },
411
+ params: () => {
412
+ const router = activeRouter();
413
+ return router ? readAccessor(router.params) : {};
414
+ },
415
+ matches: () => {
416
+ const router = activeRouter();
417
+ return router ? readAccessor(router.matches) : [];
418
+ },
419
+ navigate: defaultNavigate,
420
+ isRouting: () => {
421
+ const router = activeRouter();
422
+ return router ? readAccessor(router.isRouting) : false;
423
+ },
424
+ pendingLocation: () => {
425
+ const router = activeRouter();
426
+ return router ? readAccessor(router.pendingLocation) : null;
357
427
  },
358
- isRouting: () => false,
359
- pendingLocation: () => null,
360
- base: "",
361
- resolvePath: (to) => typeof to === "string" ? to : to.pathname || "/"
428
+ base: wrapValue(""),
429
+ resolvePath: defaultResolvePath
362
430
  };
363
431
  var RouterContext = createContext(defaultRouterContext);
364
432
  RouterContext.displayName = "RouterContext";
@@ -369,17 +437,50 @@ var defaultRouteContext = {
369
437
  match: () => void 0,
370
438
  data: () => void 0,
371
439
  outlet: () => null,
372
- resolvePath: (to) => typeof to === "string" ? to : to.pathname || "/"
440
+ resolvePath: wrapAccessor((to) => typeof to === "string" ? to : to.pathname || "/")
373
441
  };
374
442
  var RouteContext = createContext(defaultRouteContext);
375
443
  RouteContext.displayName = "RouteContext";
376
444
  function useRoute() {
377
445
  return useContext(RouteContext);
378
446
  }
447
+ var activeBeforeLeave = createSignal(null);
448
+ var activeBeforeLeaveStack = [];
449
+ function pushActiveBeforeLeave(context) {
450
+ activeBeforeLeaveStack.push(context);
451
+ batch(() => {
452
+ activeBeforeLeave(context);
453
+ });
454
+ }
455
+ function popActiveBeforeLeave(context) {
456
+ const index = activeBeforeLeaveStack.lastIndexOf(context);
457
+ if (index >= 0) {
458
+ activeBeforeLeaveStack.splice(index, 1);
459
+ }
460
+ batch(() => {
461
+ activeBeforeLeave(activeBeforeLeaveStack[activeBeforeLeaveStack.length - 1] ?? null);
462
+ });
463
+ }
379
464
  var defaultBeforeLeaveContext = {
380
- addHandler: () => () => {
381
- },
382
- confirm: async () => true
465
+ addHandler: wrapAccessor((handler) => {
466
+ const context = activeBeforeLeave();
467
+ if (context) {
468
+ return readAccessor(
469
+ context.addHandler
470
+ )(handler);
471
+ }
472
+ return () => {
473
+ };
474
+ }),
475
+ confirm: wrapAccessor((to, from) => {
476
+ const context = activeBeforeLeave();
477
+ if (context) {
478
+ return readAccessor(
479
+ context.confirm
480
+ )(to, from);
481
+ }
482
+ return Promise.resolve(true);
483
+ })
383
484
  };
384
485
  var BeforeLeaveContext = createContext(defaultBeforeLeaveContext);
385
486
  BeforeLeaveContext.displayName = "BeforeLeaveContext";
@@ -394,27 +495,28 @@ var RouteErrorContext = createContext(defaultRouteErrorContext);
394
495
  RouteErrorContext.displayName = "RouteErrorContext";
395
496
  function useNavigate() {
396
497
  const router = useRouter();
397
- return router.navigate;
498
+ return readAccessor(router.navigate);
398
499
  }
399
500
  function useLocation() {
400
501
  const router = useRouter();
401
- return router.location;
502
+ return () => readAccessor(router.location);
402
503
  }
403
504
  function useParams() {
404
505
  const router = useRouter();
405
- return router.params;
506
+ return () => readAccessor(router.params);
406
507
  }
407
508
  function useSearchParams() {
408
509
  const router = useRouter();
409
510
  const getSearchParams = () => {
410
- const location = router.location();
511
+ const location = readAccessor(router.location);
411
512
  return new URLSearchParams(location.search);
412
513
  };
413
514
  const setSearchParams = (params, options) => {
414
515
  const searchParams = params instanceof URLSearchParams ? params : new URLSearchParams(params);
415
516
  const search = searchParams.toString();
416
- const location = router.location();
417
- router.navigate(
517
+ const location = readAccessor(router.location);
518
+ const navigate = readAccessor(router.navigate);
519
+ navigate(
418
520
  {
419
521
  pathname: location.pathname,
420
522
  search: search ? "?" + search : "",
@@ -423,23 +525,23 @@ function useSearchParams() {
423
525
  { replace: options?.replace }
424
526
  );
425
527
  };
426
- return [getSearchParams, setSearchParams];
528
+ return [getSearchParams, wrapAccessor(setSearchParams)];
427
529
  }
428
530
  function useMatches() {
429
531
  const router = useRouter();
430
- return router.matches;
532
+ return () => readAccessor(router.matches);
431
533
  }
432
534
  function useIsRouting() {
433
535
  const router = useRouter();
434
- return router.isRouting;
536
+ return () => readAccessor(router.isRouting);
435
537
  }
436
538
  function usePendingLocation() {
437
539
  const router = useRouter();
438
- return router.pendingLocation;
540
+ return () => readAccessor(router.pendingLocation);
439
541
  }
440
542
  function useRouteData() {
441
543
  const route = useRoute();
442
- return route.data;
544
+ return () => readAccessor(route.data);
443
545
  }
444
546
  function useRouteError() {
445
547
  const errorContext = useContext(RouteErrorContext);
@@ -447,20 +549,23 @@ function useRouteError() {
447
549
  return errorContext.error;
448
550
  }
449
551
  const route = useRoute();
450
- return route.error?.();
552
+ const routeError = route.error;
553
+ if (routeError === void 0) return void 0;
554
+ return readAccessor(routeError);
451
555
  }
452
556
  function useResolvedPath(to) {
453
557
  const route = useRoute();
454
558
  return () => {
455
559
  const target = typeof to === "function" ? to() : to;
456
- return route.resolvePath(target);
560
+ const resolvePath2 = readAccessor(route.resolvePath);
561
+ return resolvePath2(target);
457
562
  };
458
563
  }
459
564
  function useMatch(path) {
460
565
  const router = useRouter();
461
566
  return () => {
462
567
  const targetPath = typeof path === "function" ? path() : path;
463
- const matches = router.matches();
568
+ const matches = readAccessor(router.matches);
464
569
  for (const match of matches) {
465
570
  if (match.pattern === targetPath || match.pathname === targetPath) {
466
571
  return match;
@@ -473,6 +578,7 @@ function useHref(to) {
473
578
  const router = useRouter();
474
579
  return () => {
475
580
  const target = typeof to === "function" ? to() : to;
581
+ const base = readAccessor(router.base);
476
582
  let pathname;
477
583
  let search = "";
478
584
  let hash = "";
@@ -496,16 +602,17 @@ function useHref(to) {
496
602
  }
497
603
  let resolved;
498
604
  if (pathname === "") {
499
- const currentPathname = router.location().pathname;
500
- const normalizedBase = router.base === "/" || router.base === "" ? "" : router.base;
605
+ const currentPathname = readAccessor(router.location).pathname;
606
+ const normalizedBase = base === "/" || base === "" ? "" : base;
501
607
  if (normalizedBase && !currentPathname.startsWith(normalizedBase)) {
502
608
  return currentPathname + search + hash;
503
609
  }
504
- resolved = stripBasePath(currentPathname, router.base);
610
+ resolved = stripBasePath(currentPathname, base);
505
611
  } else {
506
- resolved = router.resolvePath(pathname);
612
+ const resolvePath2 = readAccessor(router.resolvePath);
613
+ resolved = resolvePath2(pathname);
507
614
  }
508
- const baseHref = prependBasePath(resolved, router.base);
615
+ const baseHref = prependBasePath(resolved, base);
509
616
  return baseHref + search + hash;
510
617
  };
511
618
  }
@@ -513,12 +620,14 @@ function useIsActive(to, options) {
513
620
  const router = useRouter();
514
621
  return () => {
515
622
  const target = typeof to === "function" ? to() : to;
516
- const resolvedTargetPath = router.resolvePath(target);
517
- const currentPath = router.location().pathname;
518
- if (router.base && currentPath !== router.base && !currentPath.startsWith(router.base + "/")) {
623
+ const resolvePath2 = readAccessor(router.resolvePath);
624
+ const resolvedTargetPath = resolvePath2(target);
625
+ const currentPath = readAccessor(router.location).pathname;
626
+ const base = readAccessor(router.base);
627
+ if (base && currentPath !== base && !currentPath.startsWith(base + "/")) {
519
628
  return false;
520
629
  }
521
- const currentPathWithoutBase = stripBasePath(currentPath, router.base);
630
+ const currentPathWithoutBase = stripBasePath(currentPath, base);
522
631
  if (options?.end) {
523
632
  return currentPathWithoutBase === resolvedTargetPath;
524
633
  }
@@ -998,6 +1107,37 @@ function createStaticHistory(url) {
998
1107
  };
999
1108
  }
1000
1109
 
1110
+ // src/router-internals.ts
1111
+ var isDevEnv = typeof import.meta !== "undefined" && import.meta.env?.DEV === true || typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1112
+ var didWarnBaseMismatch = false;
1113
+ function hasBasePrefix(pathname, base) {
1114
+ if (!base) return true;
1115
+ return pathname === base || pathname.startsWith(base + "/");
1116
+ }
1117
+ function stripBaseOrWarn(pathname, base) {
1118
+ if (!base) return pathname;
1119
+ if (!hasBasePrefix(pathname, base)) {
1120
+ if (isDevEnv && !didWarnBaseMismatch) {
1121
+ didWarnBaseMismatch = true;
1122
+ console.warn(
1123
+ `[fict-router] Location "${pathname}" does not start with base "${base}". No routes matched.`
1124
+ );
1125
+ }
1126
+ return null;
1127
+ }
1128
+ return stripBasePath(pathname, base);
1129
+ }
1130
+ function stripBaseIfPresent(pathname, base) {
1131
+ if (!base) return pathname;
1132
+ if (!hasBasePrefix(pathname, base)) return pathname;
1133
+ return stripBasePath(pathname, base);
1134
+ }
1135
+
1136
+ // src/router-provider.ts
1137
+ import { batch as batch2, onCleanup, startTransition, untrack } from "@fictjs/runtime";
1138
+ import { createSignal as createSignal2 } from "@fictjs/runtime/advanced";
1139
+ import { jsx } from "@fictjs/runtime/jsx-runtime";
1140
+
1001
1141
  // src/scroll.ts
1002
1142
  var scrollPositions = /* @__PURE__ */ new Map();
1003
1143
  var MAX_STORED_POSITIONS = 100;
@@ -1120,34 +1260,7 @@ function configureScrollRestoration(options) {
1120
1260
  defaultScrollRestoration = createScrollRestoration(options);
1121
1261
  }
1122
1262
 
1123
- // src/components.tsx
1124
- import { Fragment as Fragment2, jsx } from "fict/jsx-runtime";
1125
- var isDevEnv = typeof import.meta !== "undefined" && import.meta.env?.DEV === true || typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
1126
- var didWarnBaseMismatch = false;
1127
- function hasBasePrefix(pathname, base) {
1128
- if (!base) return true;
1129
- return pathname === base || pathname.startsWith(base + "/");
1130
- }
1131
- function stripBaseOrWarn(pathname, base) {
1132
- if (!base) return pathname;
1133
- if (!hasBasePrefix(pathname, base)) {
1134
- if (isDevEnv && !didWarnBaseMismatch) {
1135
- didWarnBaseMismatch = true;
1136
- console.warn(
1137
- `[fict-router] Location "${pathname}" does not start with base "${base}". No routes matched.`
1138
- );
1139
- }
1140
- return null;
1141
- }
1142
- return stripBasePath(pathname, base);
1143
- }
1144
- function stripBaseIfPresent(pathname, base) {
1145
- if (!base) return pathname;
1146
- if (hasBasePrefix(pathname, base)) {
1147
- return stripBasePath(pathname, base);
1148
- }
1149
- return pathname;
1150
- }
1263
+ // src/router-provider.ts
1151
1264
  function createRouterState(history2, routes, base = "") {
1152
1265
  const normalizedBase = normalizePath(base);
1153
1266
  const baseForStrip = normalizedBase === "/" ? "" : normalizedBase;
@@ -1160,10 +1273,10 @@ function createRouterState(history2, routes, base = "") {
1160
1273
  };
1161
1274
  const initialLocation = history2.location;
1162
1275
  const initialMatches = matchWithBase(initialLocation.pathname);
1163
- const locationSignal = createSignal(initialLocation);
1164
- const matchesSignal = createSignal(initialMatches);
1165
- const isRoutingSignal = createSignal(false);
1166
- const pendingLocationSignal = createSignal(null);
1276
+ const locationSignal = createSignal2(initialLocation);
1277
+ const matchesSignal = createSignal2(initialMatches);
1278
+ const isRoutingSignal = createSignal2(false);
1279
+ const pendingLocationSignal = createSignal2(null);
1167
1280
  const beforeLeaveHandlers = /* @__PURE__ */ new Set();
1168
1281
  let navigationToken = 0;
1169
1282
  const beforeLeave = {
@@ -1174,7 +1287,7 @@ function createRouterState(history2, routes, base = "") {
1174
1287
  async confirm(to, from) {
1175
1288
  if (beforeLeaveHandlers.size === 0) return true;
1176
1289
  const currentToken = ++navigationToken;
1177
- let defaultPrevented = false;
1290
+ let defaultPrevented = true;
1178
1291
  let retryRequested = false;
1179
1292
  let forceRetry = false;
1180
1293
  const event = {
@@ -1271,12 +1384,14 @@ function createRouterState(history2, routes, base = "") {
1271
1384
  }
1272
1385
  const targetLocation = createLocation(locationSpec, finalState, toKey);
1273
1386
  untrack(async () => {
1387
+ if (beforeLeaveHandlers.size > 0) {
1388
+ pendingLocationSignal(targetLocation);
1389
+ }
1274
1390
  const canNavigate = await beforeLeave.confirm(targetLocation, currentLocation);
1275
1391
  if (!canNavigate) {
1276
- pendingLocationSignal(null);
1277
1392
  return;
1278
1393
  }
1279
- batch(() => {
1394
+ batch2(() => {
1280
1395
  isRoutingSignal(true);
1281
1396
  pendingLocationSignal(targetLocation);
1282
1397
  });
@@ -1296,7 +1411,7 @@ function createRouterState(history2, routes, base = "") {
1296
1411
  );
1297
1412
  }
1298
1413
  if (locationsAreEqual(prevLocation, history2.location)) {
1299
- batch(() => {
1414
+ batch2(() => {
1300
1415
  isRoutingSignal(false);
1301
1416
  pendingLocationSignal(null);
1302
1417
  });
@@ -1306,7 +1421,7 @@ function createRouterState(history2, routes, base = "") {
1306
1421
  };
1307
1422
  const unlisten = history2.listen(({ action: action2, location: newLocation }) => {
1308
1423
  const prevLocation = locationSignal();
1309
- batch(() => {
1424
+ batch2(() => {
1310
1425
  locationSignal(newLocation);
1311
1426
  const newMatches = matchWithBase(newLocation.pathname);
1312
1427
  matchesSignal(newMatches);
@@ -1339,6 +1454,17 @@ function RouterProvider(props) {
1339
1454
  props.base
1340
1455
  );
1341
1456
  onCleanup(cleanup);
1457
+ const beforeLeaveContext = {
1458
+ addHandler: wrapAccessor(beforeLeave.addHandler),
1459
+ confirm: wrapAccessor(beforeLeave.confirm)
1460
+ };
1461
+ const resolvePathFn = (to) => {
1462
+ const location = state().location;
1463
+ const currentPathWithoutBase = stripBaseOrWarn(location.pathname, normalizedBase) || "/";
1464
+ const rawTargetPath = typeof to === "string" ? to : to.pathname || "/";
1465
+ const targetPath = rawTargetPath.startsWith("/") ? stripBaseIfPresent(rawTargetPath, normalizedBase) : rawTargetPath;
1466
+ return resolvePath(currentPathWithoutBase, targetPath);
1467
+ };
1342
1468
  const routerContext = {
1343
1469
  location: () => state().location,
1344
1470
  params: () => {
@@ -1350,30 +1476,41 @@ function RouterProvider(props) {
1350
1476
  return allParams;
1351
1477
  },
1352
1478
  matches: () => state().matches,
1353
- navigate,
1479
+ navigate: wrapAccessor(navigate),
1354
1480
  isRouting: () => state().isRouting,
1355
1481
  pendingLocation: () => state().pendingLocation,
1356
- base: normalizedBase,
1357
- resolvePath: (to) => {
1358
- const location = state().location;
1359
- const currentPathWithoutBase = stripBaseOrWarn(location.pathname, normalizedBase) || "/";
1360
- const rawTargetPath = typeof to === "string" ? to : to.pathname || "/";
1361
- const targetPath = rawTargetPath.startsWith("/") ? stripBaseIfPresent(rawTargetPath, normalizedBase) : rawTargetPath;
1362
- return resolvePath(currentPathWithoutBase, targetPath);
1363
- }
1482
+ base: wrapValue(normalizedBase),
1483
+ resolvePath: wrapAccessor(resolvePathFn)
1364
1484
  };
1365
- return /* @__PURE__ */ jsx(RouterContext.Provider, { value: routerContext, children: /* @__PURE__ */ jsx(BeforeLeaveContext.Provider, { value: beforeLeave, children: props.children }) });
1485
+ pushActiveRouter(routerContext);
1486
+ pushActiveBeforeLeave(beforeLeaveContext);
1487
+ onCleanup(() => {
1488
+ popActiveBeforeLeave(beforeLeaveContext);
1489
+ popActiveRouter(routerContext);
1490
+ });
1491
+ const RouterContextProvider = RouterContext.Provider;
1492
+ const BeforeLeaveProvider = BeforeLeaveContext.Provider;
1493
+ return jsx(RouterContextProvider, {
1494
+ value: routerContext,
1495
+ children: jsx(BeforeLeaveProvider, {
1496
+ value: beforeLeaveContext,
1497
+ children: props.children
1498
+ })
1499
+ });
1366
1500
  }
1501
+
1502
+ // src/components.tsx
1503
+ import { Fragment as Fragment2, jsx as jsx2 } from "fict/jsx-runtime";
1367
1504
  function Router(props) {
1368
1505
  const history2 = props.history || createBrowserHistory();
1369
1506
  const routes = extractRoutes(props.children);
1370
- return /* @__PURE__ */ jsx(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ jsx(Routes, { children: props.children }) });
1507
+ return /* @__PURE__ */ jsx2(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ jsx2(Routes, { children: props.children }) });
1371
1508
  }
1372
1509
  function HashRouter(props) {
1373
1510
  const hashOptions = props.hashType ? { hashType: props.hashType } : void 0;
1374
1511
  const history2 = createHashHistory(hashOptions);
1375
1512
  const routes = extractRoutes(props.children);
1376
- return /* @__PURE__ */ jsx(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ jsx(Routes, { children: props.children }) });
1513
+ return /* @__PURE__ */ jsx2(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ jsx2(Routes, { children: props.children }) });
1377
1514
  }
1378
1515
  function MemoryRouter(props) {
1379
1516
  const memoryOptions = {};
@@ -1387,12 +1524,12 @@ function MemoryRouter(props) {
1387
1524
  Object.keys(memoryOptions).length > 0 ? memoryOptions : void 0
1388
1525
  );
1389
1526
  const routes = extractRoutes(props.children);
1390
- return /* @__PURE__ */ jsx(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ jsx(Routes, { children: props.children }) });
1527
+ return /* @__PURE__ */ jsx2(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ jsx2(Routes, { children: props.children }) });
1391
1528
  }
1392
1529
  function StaticRouter(props) {
1393
1530
  const history2 = createStaticHistory(props.url);
1394
1531
  const routes = extractRoutes(props.children);
1395
- return /* @__PURE__ */ jsx(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ jsx(Routes, { children: props.children }) });
1532
+ return /* @__PURE__ */ jsx2(RouterProvider, { history: history2, routes, base: props.base, children: /* @__PURE__ */ jsx2(Routes, { children: props.children }) });
1396
1533
  }
1397
1534
  function Routes(props) {
1398
1535
  const router = useRouter();
@@ -1401,9 +1538,11 @@ function Routes(props) {
1401
1538
  const compiledRoutes = routes.map((r) => compileRoute(r));
1402
1539
  const branches = createBranches(compiledRoutes);
1403
1540
  const currentMatches = createMemo(() => {
1404
- const location = router.location();
1405
- const parentMatch = parentRoute.match();
1406
- const locationPath = stripBaseOrWarn(location.pathname, router.base);
1541
+ const pendingLocation = readAccessor(router.pendingLocation);
1542
+ const location = pendingLocation ?? readAccessor(router.location);
1543
+ const parentMatch = readAccessor(parentRoute.match);
1544
+ const base = readAccessor(router.base);
1545
+ const locationPath = stripBaseOrWarn(location.pathname, base);
1407
1546
  if (locationPath == null) return [];
1408
1547
  let basePath = "/";
1409
1548
  if (parentMatch) {
@@ -1412,16 +1551,14 @@ function Routes(props) {
1412
1551
  const relativePath = locationPath.startsWith(basePath) ? locationPath.slice(basePath.length) || "/" : locationPath;
1413
1552
  return matchRoutes(branches, relativePath) || [];
1414
1553
  });
1415
- return /* @__PURE__ */ jsx(Fragment2, { children: renderMatches(currentMatches(), 0) });
1554
+ const matches = currentMatches();
1555
+ return /* @__PURE__ */ jsx2(Fragment2, { children: matches.length > 0 ? renderMatches(matches, 0) : null });
1416
1556
  }
1417
1557
  function renderMatches(matches, index) {
1418
- if (index >= matches.length) {
1419
- return null;
1420
- }
1421
1558
  const match = matches[index];
1422
1559
  const route = match.route;
1423
1560
  const router = useRouter();
1424
- const dataState = createSignal({
1561
+ const dataState = createSignal3({
1425
1562
  data: void 0,
1426
1563
  error: void 0,
1427
1564
  loading: !!route.preload
@@ -1429,7 +1566,7 @@ function renderMatches(matches, index) {
1429
1566
  let preloadToken = 0;
1430
1567
  if (route.preload) {
1431
1568
  createEffect(() => {
1432
- const location = router.location();
1569
+ const location = readAccessor(router.location);
1433
1570
  const preloadArgs = {
1434
1571
  params: match.params,
1435
1572
  location,
@@ -1448,17 +1585,7 @@ function renderMatches(matches, index) {
1448
1585
  });
1449
1586
  });
1450
1587
  }
1451
- const routeContext = {
1452
- match: () => match,
1453
- data: () => dataState().data,
1454
- error: () => dataState().error,
1455
- outlet: () => renderMatches(matches, index + 1),
1456
- resolvePath: (to) => {
1457
- const basePath = match.pathname;
1458
- const targetPath = typeof to === "string" ? to : to.pathname || "/";
1459
- return resolvePath(basePath, targetPath);
1460
- }
1461
- };
1588
+ const outletNode = /* @__PURE__ */ jsx2(Outlet, {});
1462
1589
  const renderContent = () => {
1463
1590
  const state = dataState();
1464
1591
  if (state.error !== void 0 && route.errorElement) {
@@ -1469,26 +1596,39 @@ function renderMatches(matches, index) {
1469
1596
  }
1470
1597
  if (route.component) {
1471
1598
  const Component = route.component;
1472
- return /* @__PURE__ */ jsx(Component, { params: match.params, location: router.location(), data: state.data, children: /* @__PURE__ */ jsx(Outlet, {}) });
1473
- } else if (route.element) {
1599
+ return /* @__PURE__ */ jsx2(Component, { params: match.params, location: readAccessor(router.location), data: state.data, children: outletNode });
1600
+ }
1601
+ if (route.element) {
1474
1602
  return route.element;
1475
- } else if (route.children) {
1476
- return /* @__PURE__ */ jsx(Outlet, {});
1603
+ }
1604
+ if (route.children) {
1605
+ return outletNode;
1477
1606
  }
1478
1607
  return null;
1479
1608
  };
1480
- let content = /* @__PURE__ */ jsx(RouteContext.Provider, { value: routeContext, children: renderContent() });
1609
+ const routeContext = {
1610
+ match: () => match,
1611
+ data: () => dataState().data,
1612
+ error: () => dataState().error,
1613
+ outlet: () => index + 1 < matches.length ? renderMatches(matches, index + 1) : null,
1614
+ resolvePath: wrapAccessor((to) => {
1615
+ const basePath = match.pathname;
1616
+ const targetPath = typeof to === "string" ? to : to.pathname || "/";
1617
+ return resolvePath(basePath, targetPath);
1618
+ })
1619
+ };
1620
+ let content = /* @__PURE__ */ jsx2(RouteContext.Provider, { value: routeContext, children: renderContent() });
1481
1621
  if (route.errorElement) {
1482
- content = /* @__PURE__ */ jsx(
1622
+ content = /* @__PURE__ */ jsx2(
1483
1623
  ErrorBoundary,
1484
1624
  {
1485
- fallback: (err, reset) => /* @__PURE__ */ jsx(RouteErrorContext.Provider, { value: { error: err, reset }, children: route.errorElement }),
1625
+ fallback: (err, reset) => /* @__PURE__ */ jsx2(RouteErrorContext.Provider, { value: { error: err, reset }, children: route.errorElement }),
1486
1626
  children: content
1487
1627
  }
1488
1628
  );
1489
1629
  }
1490
1630
  if (route.loadingElement) {
1491
- content = /* @__PURE__ */ jsx(Suspense, { fallback: route.loadingElement, children: content });
1631
+ content = /* @__PURE__ */ jsx2(Suspense, { fallback: route.loadingElement, children: content });
1492
1632
  }
1493
1633
  return content;
1494
1634
  }
@@ -1497,7 +1637,7 @@ function Route(_props) {
1497
1637
  }
1498
1638
  function Outlet() {
1499
1639
  const route = useRoute();
1500
- return /* @__PURE__ */ jsx(Fragment2, { children: route.outlet() });
1640
+ return readAccessor(route.outlet);
1501
1641
  }
1502
1642
  function Navigate(props) {
1503
1643
  const router = useRouter();
@@ -1554,28 +1694,42 @@ function createRouter(routes, options) {
1554
1694
  return {
1555
1695
  Router: (props) => {
1556
1696
  const history2 = options?.history || createBrowserHistory();
1557
- return /* @__PURE__ */ jsx(RouterProvider, { history: history2, routes, base: options?.base, children: props.children || /* @__PURE__ */ jsx(Routes, { children: routesToElements(routes) }) });
1697
+ return /* @__PURE__ */ jsx2(RouterProvider, { history: history2, routes, base: options?.base, children: props.children || /* @__PURE__ */ jsx2(Routes, { children: routesToElements(routes) }) });
1558
1698
  }
1559
1699
  };
1560
1700
  }
1561
1701
  function routesToElements(routes) {
1562
- return /* @__PURE__ */ jsx(Fragment2, { children: routes.map((route, i) => {
1702
+ return /* @__PURE__ */ jsx2(Fragment2, { children: routes.map((route, i) => {
1563
1703
  const routeProps = { key: route.key || `route-${i}` };
1564
1704
  if (route.path !== void 0) routeProps.path = route.path;
1565
1705
  if (route.component !== void 0) routeProps.component = route.component;
1566
1706
  if (route.element !== void 0) routeProps.element = route.element;
1567
1707
  if (route.index !== void 0) routeProps.index = route.index;
1568
1708
  if (route.children) routeProps.children = routesToElements(route.children);
1569
- return /* @__PURE__ */ jsx(Route, { ...routeProps });
1709
+ return /* @__PURE__ */ jsx2(Route, { ...routeProps });
1570
1710
  }) });
1571
1711
  }
1572
1712
 
1573
1713
  // src/link.tsx
1574
1714
  import { createMemo as createMemo2 } from "@fictjs/runtime";
1575
- import { jsx as jsx2 } from "fict/jsx-runtime";
1715
+ import { spread } from "@fictjs/runtime/internal";
1716
+ import { jsx as jsx3 } from "fict/jsx-runtime";
1717
+ var createSpreadRef = (props) => {
1718
+ let current = null;
1719
+ return (el) => {
1720
+ if (!el) {
1721
+ current = null;
1722
+ return;
1723
+ }
1724
+ if (el === current) return;
1725
+ current = el;
1726
+ spread(el, props, false, true);
1727
+ };
1728
+ };
1576
1729
  function Link(props) {
1577
1730
  const router = useRouter();
1578
1731
  const href = useHref(() => props.to);
1732
+ const getHrefValue = () => readAccessor(readAccessor(href));
1579
1733
  let preloadTriggered = false;
1580
1734
  const handleClick = (event) => {
1581
1735
  if (props.onClick) {
@@ -1600,7 +1754,7 @@ function Link(props) {
1600
1754
  const triggerPreload = () => {
1601
1755
  if (preloadTriggered || props.disabled || props.prefetch === "none") return;
1602
1756
  preloadTriggered = true;
1603
- const hrefValue = href();
1757
+ const hrefValue = getHrefValue();
1604
1758
  if (typeof window !== "undefined" && window.dispatchEvent) {
1605
1759
  window.dispatchEvent(
1606
1760
  new CustomEvent("fict-router:preload", {
@@ -1613,14 +1767,16 @@ function Link(props) {
1613
1767
  if (props.prefetch === "intent" || props.prefetch === void 0) {
1614
1768
  triggerPreload();
1615
1769
  }
1616
- const onMouseEnter = props.onMouseEnter;
1770
+ const propsRecord = props;
1771
+ const onMouseEnter = propsRecord.onMouseEnter;
1617
1772
  if (onMouseEnter) onMouseEnter(event);
1618
1773
  };
1619
1774
  const handleFocus = (event) => {
1620
1775
  if (props.prefetch === "intent" || props.prefetch === void 0) {
1621
1776
  triggerPreload();
1622
1777
  }
1623
- const onFocus = props.onFocus;
1778
+ const propsRecord = props;
1779
+ const onFocus = propsRecord.onFocus;
1624
1780
  if (onFocus) onFocus(event);
1625
1781
  };
1626
1782
  const {
@@ -1633,20 +1789,24 @@ function Link(props) {
1633
1789
  prefetch,
1634
1790
  disabled,
1635
1791
  onClick: _onClick,
1792
+ onMouseEnter: _onMouseEnter,
1793
+ onFocus: _onFocus,
1636
1794
  children,
1637
1795
  ...anchorProps
1638
1796
  } = props;
1797
+ const anchorRef = createSpreadRef(anchorProps);
1798
+ const spanRef = createSpreadRef(anchorProps);
1639
1799
  if (disabled) {
1640
- return /* @__PURE__ */ jsx2("span", { ...anchorProps, children });
1800
+ return /* @__PURE__ */ jsx3("span", { ref: spanRef, children });
1641
1801
  }
1642
1802
  if (prefetch === "render") {
1643
1803
  triggerPreload();
1644
1804
  }
1645
- return /* @__PURE__ */ jsx2(
1805
+ return /* @__PURE__ */ jsx3(
1646
1806
  "a",
1647
1807
  {
1648
- ...anchorProps,
1649
- href: href(),
1808
+ ref: anchorRef,
1809
+ href: getHrefValue(),
1650
1810
  onClick: handleClick,
1651
1811
  onMouseEnter: handleMouseEnter,
1652
1812
  onFocus: handleFocus,
@@ -1658,12 +1818,14 @@ function NavLink(props) {
1658
1818
  const router = useRouter();
1659
1819
  const isActive = useIsActive(() => props.to, { end: props.end });
1660
1820
  const href = useHref(() => props.to);
1821
+ const getHrefValue = () => readAccessor(readAccessor(href));
1661
1822
  const pendingLocation = usePendingLocation();
1662
1823
  const computeIsPending = () => {
1663
1824
  const pending = pendingLocation();
1664
1825
  if (!pending) return false;
1665
- const resolvedHref = href();
1666
- const baseToStrip = router.base === "/" ? "" : router.base;
1826
+ const resolvedHref = getHrefValue();
1827
+ const base = readAccessor(router.base);
1828
+ const baseToStrip = base === "/" ? "" : base;
1667
1829
  const pendingPathWithoutBase = stripBasePath(pending.pathname, baseToStrip);
1668
1830
  const parsed = parseURL(resolvedHref);
1669
1831
  const targetPathWithoutBase = stripBasePath(parsed.pathname, baseToStrip);
@@ -1675,7 +1837,7 @@ function NavLink(props) {
1675
1837
  const getRenderProps = () => ({
1676
1838
  isActive: isActive(),
1677
1839
  isPending: computeIsPending(),
1678
- isTransitioning: router.isRouting()
1840
+ isTransitioning: readAccessor(router.isRouting)
1679
1841
  });
1680
1842
  const computedClassName = createMemo2(() => {
1681
1843
  const renderProps = getRenderProps();
@@ -1764,18 +1926,30 @@ function NavLink(props) {
1764
1926
  "aria-current": _ariaCurrent,
1765
1927
  ...anchorProps
1766
1928
  } = props;
1929
+ const anchorRef = createSpreadRef(anchorProps);
1930
+ const spanRef = createSpreadRef(anchorProps);
1767
1931
  if (disabled) {
1768
- return /* @__PURE__ */ jsx2("span", { ...anchorProps, className: computedClassName(), style: computedStyle(), children: computedChildren() });
1932
+ const disabledClassName = computedClassName();
1933
+ const disabledStyle = computedStyle();
1934
+ return /* @__PURE__ */ jsx3(
1935
+ "span",
1936
+ {
1937
+ ref: spanRef,
1938
+ ...disabledClassName !== void 0 ? { class: disabledClassName } : {},
1939
+ ...disabledStyle !== void 0 ? { style: disabledStyle } : {},
1940
+ children: computedChildren()
1941
+ }
1942
+ );
1769
1943
  }
1770
1944
  const finalClassName = computedClassName();
1771
1945
  const finalStyle = computedStyle();
1772
1946
  const finalAriaCurrent = ariaCurrent();
1773
- return /* @__PURE__ */ jsx2(
1947
+ return /* @__PURE__ */ jsx3(
1774
1948
  "a",
1775
1949
  {
1776
- ...anchorProps,
1777
- href: href(),
1778
- ...finalClassName !== void 0 ? { className: finalClassName } : {},
1950
+ ref: anchorRef,
1951
+ href: getHrefValue(),
1952
+ ...finalClassName !== void 0 ? { class: finalClassName } : {},
1779
1953
  ...finalStyle !== void 0 ? { style: finalStyle } : {},
1780
1954
  ...finalAriaCurrent !== void 0 ? { "aria-current": finalAriaCurrent } : {},
1781
1955
  onClick: handleClick,
@@ -1796,7 +1970,7 @@ function Form(props) {
1796
1970
  event.preventDefault();
1797
1971
  const formData = new FormData(form);
1798
1972
  const method2 = props.method?.toUpperCase() || "GET";
1799
- const actionUrl = props.action || router.location().pathname;
1973
+ const actionUrl = props.action || readAccessor(router.location).pathname;
1800
1974
  if (method2 === "GET") {
1801
1975
  const searchParams = new URLSearchParams();
1802
1976
  formData.forEach((value, key) => {
@@ -1871,11 +2045,12 @@ function Form(props) {
1871
2045
  onSubmit: _onSubmit,
1872
2046
  ...formProps
1873
2047
  } = props;
2048
+ const formRef = createSpreadRef(formProps);
1874
2049
  const htmlMethod = method && ["get", "post"].includes(method) ? method : void 0;
1875
- return /* @__PURE__ */ jsx2(
2050
+ return /* @__PURE__ */ jsx3(
1876
2051
  "form",
1877
2052
  {
1878
- ...formProps,
2053
+ ref: formRef,
1879
2054
  ...action2 !== void 0 ? { action: action2 } : {},
1880
2055
  ...htmlMethod !== void 0 ? { method: htmlMethod } : {},
1881
2056
  onSubmit: handleSubmit,
@@ -1885,8 +2060,8 @@ function Form(props) {
1885
2060
  }
1886
2061
 
1887
2062
  // src/data.ts
1888
- import { createEffect as createEffect2, batch as batch2 } from "@fictjs/runtime";
1889
- import { createSignal as createSignal2 } from "@fictjs/runtime/advanced";
2063
+ import { createEffect as createEffect2, batch as batch3 } from "@fictjs/runtime";
2064
+ import { createSignal as createSignal4 } from "@fictjs/runtime/advanced";
1890
2065
  var CACHE_DURATION = 3 * 60 * 1e3;
1891
2066
  var PRELOAD_CACHE_DURATION = 5 * 1e3;
1892
2067
  var MAX_CACHE_SIZE = 500;
@@ -1939,9 +2114,9 @@ function query(fn, name) {
1939
2114
  return () => cached.result;
1940
2115
  }
1941
2116
  }
1942
- const resultSignal = createSignal2(cached?.result);
1943
- const errorSignal = createSignal2(void 0);
1944
- const loadingSignal = createSignal2(true);
2117
+ const resultSignal = createSignal4(cached?.result);
2118
+ const errorSignal = createSignal4(void 0);
2119
+ const loadingSignal = createSignal4(true);
1945
2120
  const promise = Promise.resolve(fn(...args)).then((result) => {
1946
2121
  const entry = {
1947
2122
  timestamp: Date.now(),
@@ -1951,13 +2126,13 @@ function query(fn, name) {
1951
2126
  };
1952
2127
  queryCache.set(cacheKey, entry);
1953
2128
  evictOldestEntries();
1954
- batch2(() => {
2129
+ batch3(() => {
1955
2130
  resultSignal(result);
1956
2131
  loadingSignal(false);
1957
2132
  });
1958
2133
  return result;
1959
2134
  }).catch((error) => {
1960
- batch2(() => {
2135
+ batch3(() => {
1961
2136
  errorSignal(error);
1962
2137
  loadingSignal(false);
1963
2138
  });
@@ -2026,7 +2201,7 @@ function action(fn, name) {
2026
2201
  function getAction(url) {
2027
2202
  return actionRegistry.get(url);
2028
2203
  }
2029
- var activeSubmissions = createSignal2(/* @__PURE__ */ new Map());
2204
+ var activeSubmissions = createSignal4(/* @__PURE__ */ new Map());
2030
2205
  function useSubmission(actionOrUrl) {
2031
2206
  const url = typeof actionOrUrl === "string" ? actionOrUrl : actionOrUrl.url;
2032
2207
  return () => {
@@ -2081,10 +2256,10 @@ function createPreload(fn) {
2081
2256
  };
2082
2257
  }
2083
2258
  function createResource(source, fetcher) {
2084
- const dataSignal = createSignal2(void 0);
2085
- const loadingSignal = createSignal2(true);
2086
- const errorSignal = createSignal2(void 0);
2087
- const latestSignal = createSignal2(void 0);
2259
+ const dataSignal = createSignal4(void 0);
2260
+ const loadingSignal = createSignal4(true);
2261
+ const errorSignal = createSignal4(void 0);
2262
+ const latestSignal = createSignal4(void 0);
2088
2263
  let currentSource;
2089
2264
  let fetchId = 0;
2090
2265
  const doFetch = async (s, id) => {
@@ -2093,7 +2268,7 @@ function createResource(source, fetcher) {
2093
2268
  try {
2094
2269
  const result = await fetcher(s);
2095
2270
  if (id === fetchId) {
2096
- batch2(() => {
2271
+ batch3(() => {
2097
2272
  dataSignal(result);
2098
2273
  latestSignal(result);
2099
2274
  loadingSignal(false);
@@ -2103,7 +2278,7 @@ function createResource(source, fetcher) {
2103
2278
  return void 0;
2104
2279
  } catch (err) {
2105
2280
  if (id === fetchId) {
2106
- batch2(() => {
2281
+ batch3(() => {
2107
2282
  errorSignal(err);
2108
2283
  loadingSignal(false);
2109
2284
  });
@@ -2137,21 +2312,25 @@ function cleanupDataUtilities() {
2137
2312
  }
2138
2313
 
2139
2314
  // src/lazy.tsx
2140
- import "@fictjs/runtime";
2141
- import { createSignal as createSignal3 } from "@fictjs/runtime/advanced";
2142
- import { jsx as jsx3 } from "fict/jsx-runtime";
2315
+ import { onCleanup as onCleanup2 } from "@fictjs/runtime";
2316
+ import { createSignal as createSignal5 } from "@fictjs/runtime/advanced";
2317
+ import { jsx as jsx4 } from "fict/jsx-runtime";
2143
2318
  function lazy(loader) {
2144
2319
  let cachedComponent = null;
2145
2320
  let loadPromise = null;
2146
2321
  const LazyComponent = (props) => {
2147
- const state = createSignal3({
2322
+ let isMounted = true;
2323
+ onCleanup2(() => {
2324
+ isMounted = false;
2325
+ });
2326
+ const state = createSignal5({
2148
2327
  component: cachedComponent,
2149
2328
  error: null,
2150
2329
  loading: !cachedComponent
2151
2330
  });
2152
2331
  if (cachedComponent) {
2153
2332
  const CachedComponent = cachedComponent;
2154
- return /* @__PURE__ */ jsx3(CachedComponent, { ...props });
2333
+ return /* @__PURE__ */ jsx4(CachedComponent, { ...props });
2155
2334
  }
2156
2335
  if (!loadPromise) {
2157
2336
  loadPromise = loader().then((module) => {
@@ -2161,9 +2340,13 @@ function lazy(loader) {
2161
2340
  });
2162
2341
  }
2163
2342
  loadPromise.then((component) => {
2164
- state({ component, error: null, loading: false });
2343
+ if (isMounted) {
2344
+ state({ component, error: null, loading: false });
2345
+ }
2165
2346
  }).catch((error) => {
2166
- state({ component: null, error, loading: false });
2347
+ if (isMounted) {
2348
+ state({ component: null, error, loading: false });
2349
+ }
2167
2350
  });
2168
2351
  const currentState = state();
2169
2352
  if (currentState.error) {
@@ -2173,10 +2356,11 @@ function lazy(loader) {
2173
2356
  throw loadPromise;
2174
2357
  }
2175
2358
  const LoadedComponent = currentState.component;
2176
- return /* @__PURE__ */ jsx3(LoadedComponent, { ...props });
2359
+ return /* @__PURE__ */ jsx4(LoadedComponent, { ...props });
2177
2360
  };
2178
- LazyComponent.__lazy = true;
2179
- LazyComponent.__preload = () => {
2361
+ const lazyComp = LazyComponent;
2362
+ lazyComp.__lazy = true;
2363
+ lazyComp.__preload = () => {
2180
2364
  if (!loadPromise) {
2181
2365
  loadPromise = loader().then((module) => {
2182
2366
  const component = "default" in module ? module.default : module;
@@ -2196,7 +2380,8 @@ function preloadLazy(component) {
2196
2380
  return Promise.resolve();
2197
2381
  }
2198
2382
  function isLazyComponent(component) {
2199
- return !!(component && typeof component === "function" && component.__lazy);
2383
+ const comp = component;
2384
+ return !!(comp && typeof comp === "function" && comp.__lazy);
2200
2385
  }
2201
2386
  function lazyRoute(config) {
2202
2387
  const LazyComponent = lazy(config.component);