@solidjs/router 0.16.0 → 0.16.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.
Files changed (71) hide show
  1. package/dist/components.jsx +3 -2
  2. package/dist/data/action.js +11 -7
  3. package/dist/data/createAsync.js +3 -0
  4. package/dist/data/events.d.ts +8 -1
  5. package/dist/data/events.js +2 -2
  6. package/dist/data/query.js +5 -5
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +61 -18
  9. package/dist/index.jsx +1 -1
  10. package/dist/lifecycle.js +8 -1
  11. package/dist/routers/HashRouter.js +1 -1
  12. package/dist/routers/MemoryRouter.js +1 -1
  13. package/dist/routers/Router.js +2 -2
  14. package/dist/routers/components.d.ts +2 -2
  15. package/dist/routers/components.jsx +4 -1
  16. package/dist/routing.d.ts +2 -1
  17. package/dist/routing.js +30 -6
  18. package/dist/types.d.ts +6 -3
  19. package/dist/utils.d.ts +1 -0
  20. package/dist/utils.js +17 -1
  21. package/package.json +2 -2
  22. package/dist/src/components.d.ts +0 -31
  23. package/dist/src/components.jsx +0 -39
  24. package/dist/src/data/action.d.ts +0 -17
  25. package/dist/src/data/action.js +0 -163
  26. package/dist/src/data/action.spec.d.ts +0 -1
  27. package/dist/src/data/action.spec.js +0 -297
  28. package/dist/src/data/createAsync.d.ts +0 -32
  29. package/dist/src/data/createAsync.js +0 -96
  30. package/dist/src/data/createAsync.spec.d.ts +0 -1
  31. package/dist/src/data/createAsync.spec.js +0 -196
  32. package/dist/src/data/events.d.ts +0 -9
  33. package/dist/src/data/events.js +0 -123
  34. package/dist/src/data/events.spec.d.ts +0 -1
  35. package/dist/src/data/events.spec.js +0 -567
  36. package/dist/src/data/index.d.ts +0 -4
  37. package/dist/src/data/index.js +0 -4
  38. package/dist/src/data/query.d.ts +0 -23
  39. package/dist/src/data/query.js +0 -232
  40. package/dist/src/data/query.spec.d.ts +0 -1
  41. package/dist/src/data/query.spec.js +0 -354
  42. package/dist/src/data/response.d.ts +0 -4
  43. package/dist/src/data/response.js +0 -42
  44. package/dist/src/data/response.spec.d.ts +0 -1
  45. package/dist/src/data/response.spec.js +0 -165
  46. package/dist/src/index.d.ts +0 -7
  47. package/dist/src/index.jsx +0 -6
  48. package/dist/src/lifecycle.d.ts +0 -5
  49. package/dist/src/lifecycle.js +0 -69
  50. package/dist/src/routers/HashRouter.d.ts +0 -9
  51. package/dist/src/routers/HashRouter.js +0 -41
  52. package/dist/src/routers/MemoryRouter.d.ts +0 -24
  53. package/dist/src/routers/MemoryRouter.js +0 -57
  54. package/dist/src/routers/Router.d.ts +0 -9
  55. package/dist/src/routers/Router.js +0 -45
  56. package/dist/src/routers/StaticRouter.d.ts +0 -6
  57. package/dist/src/routers/StaticRouter.js +0 -15
  58. package/dist/src/routers/components.d.ts +0 -27
  59. package/dist/src/routers/components.jsx +0 -118
  60. package/dist/src/routers/createRouter.d.ts +0 -10
  61. package/dist/src/routers/createRouter.js +0 -41
  62. package/dist/src/routers/index.d.ts +0 -11
  63. package/dist/src/routers/index.js +0 -6
  64. package/dist/src/routing.d.ts +0 -175
  65. package/dist/src/routing.js +0 -560
  66. package/dist/src/types.d.ts +0 -200
  67. package/dist/src/types.js +0 -1
  68. package/dist/src/utils.d.ts +0 -13
  69. package/dist/src/utils.js +0 -185
  70. package/dist/test/helpers.d.ts +0 -6
  71. package/dist/test/helpers.js +0 -50
@@ -18,8 +18,9 @@ export function A(props) {
18
18
  const to_ = to();
19
19
  if (to_ === undefined)
20
20
  return [false, false];
21
- const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
22
- const loc = decodeURI(normalizePath(location.pathname).toLowerCase());
21
+ // trailing slashes are ignored so `/route` and `/route/` share active state
22
+ const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
23
+ const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
23
24
  return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
24
25
  });
25
26
  return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{
@@ -1,7 +1,7 @@
1
1
  import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js";
2
2
  import { isServer } from "solid-js/web";
3
3
  import { useRouter } from "../routing.js";
4
- import { mockBase } from "../utils.js";
4
+ import { mockBase, setFunctionName } from "../utils.js";
5
5
  import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
6
6
  export const actions = /* #__PURE__ */ new Map();
7
7
  export function useSubmissions(fn, filter) {
@@ -24,7 +24,7 @@ export function useSubmission(fn, filter) {
24
24
  const submissions = useSubmissions(fn, filter);
25
25
  return new Proxy({}, {
26
26
  get(_, property) {
27
- if ((submissions.length === 0 && property === "clear") || property === "retry")
27
+ if (submissions.length === 0 && (property === "clear" || property === "retry"))
28
28
  return () => { };
29
29
  return submissions[submissions.length - 1]?.[property];
30
30
  }
@@ -53,7 +53,7 @@ export function action(fn, options = {}) {
53
53
  error: result?.error,
54
54
  pending: false,
55
55
  retry() {
56
- return retry = submission.retry();
56
+ return (retry = submission.retry());
57
57
  }
58
58
  });
59
59
  if (retry)
@@ -93,10 +93,11 @@ export function action(fn, options = {}) {
93
93
  return p.then(handler(), handler(true));
94
94
  }
95
95
  const o = typeof options === "string" ? { name: options } : options;
96
- const url = fn.url ||
97
- (o.name && `https://action/${o.name}`) ||
98
- (!isServer ? `https://action/${hashString(fn.toString())}` : "");
96
+ const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined);
97
+ const url = fn.url || (name && `https://action/${name}`) || "";
99
98
  mutate.base = url;
99
+ if (name)
100
+ setFunctionName(mutate, name);
100
101
  return toAction(mutate, url);
101
102
  }
102
103
  function toAction(fn, url) {
@@ -117,7 +118,10 @@ function toAction(fn, url) {
117
118
  fn.url = url;
118
119
  if (!isServer) {
119
120
  actions.set(url, fn);
120
- getOwner() && onCleanup(() => actions.delete(url));
121
+ // Only remove the registration if it still belongs to this instance —
122
+ // a re-created action (e.g. a new `.with()` binding after revalidation)
123
+ // may have registered itself under the same URL since.
124
+ getOwner() && onCleanup(() => actions.get(url) === fn && actions.delete(url));
121
125
  }
122
126
  return fn;
123
127
  }
@@ -4,11 +4,14 @@
4
4
  import { createResource, sharedConfig, untrack, catchError } from "solid-js";
5
5
  import { createStore, reconcile, unwrap } from "solid-js/store";
6
6
  import { isServer } from "solid-js/web";
7
+ import { setFunctionName } from "../utils.js";
7
8
  export function createAsync(fn, options) {
8
9
  let resource;
9
10
  let prev = () => !resource || resource.state === "unresolved" ? undefined : resource.latest;
10
11
  [resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, options);
11
12
  const resultAccessor = (() => resource());
13
+ if (options?.name)
14
+ setFunctionName(resultAccessor, options.name);
12
15
  Object.defineProperty(resultAccessor, "latest", {
13
16
  get() {
14
17
  return resource.latest;
@@ -1,2 +1,9 @@
1
1
  import type { RouterContext } from "../types.js";
2
- export declare function setupNativeEvents(preload?: boolean, explicitLinks?: boolean, actionBase?: string, transformUrl?: (url: string) => string): (router: RouterContext) => void;
2
+ type NativeEventConfig = {
3
+ preload?: boolean;
4
+ explicitLinks?: boolean;
5
+ actionBase?: string;
6
+ transformUrl?: (url: string) => string;
7
+ };
8
+ export declare function setupNativeEvents({ preload, explicitLinks, actionBase, transformUrl }?: NativeEventConfig): (router: RouterContext) => void;
9
+ export {};
@@ -2,7 +2,7 @@ import { delegateEvents } from "solid-js/web";
2
2
  import { onCleanup } from "solid-js";
3
3
  import { actions } from "./action.js";
4
4
  import { mockBase } from "../utils.js";
5
- export function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "/_server", transformUrl) {
5
+ export function setupNativeEvents({ preload = true, explicitLinks = false, actionBase = "/_server", transformUrl } = {}) {
6
6
  return (router) => {
7
7
  const basePath = router.base.path();
8
8
  const navigateFromRoute = router.navigatorFactory(router.base);
@@ -65,7 +65,7 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB
65
65
  clearTimeout(preloadTimeout);
66
66
  const res = handleAnchor(evt);
67
67
  if (!res)
68
- return lastElement = null;
68
+ return (lastElement = null);
69
69
  const [a, url] = res;
70
70
  if (lastElement === a)
71
71
  return;
@@ -28,12 +28,12 @@ function getCache() {
28
28
  * Revalidates the given cache entry/entries.
29
29
  */
30
30
  export function revalidate(key, force = true) {
31
+ const now = Date.now();
32
+ // force the cache miss synchronously so a `refetch` in the same tick sees it —
33
+ // startTransition defers its callback to a microtask (#497)
34
+ force && cacheKeyOp(key, entry => (entry[0] = 0));
31
35
  return startTransition(() => {
32
- const now = Date.now();
33
- cacheKeyOp(key, entry => {
34
- force && (entry[0] = 0); //force cache miss
35
- entry[4][1](now); // retrigger live signals
36
- });
36
+ cacheKeyOp(key, entry => entry[4][1](now)); // retrigger live signals
37
37
  });
38
38
  }
39
39
  export function cacheKeyOp(key, fn) {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from "./routers/index.js";
2
2
  export * from "./components.jsx";
3
3
  export * from "./lifecycle.js";
4
- export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute } from "./routing.js";
4
+ export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute, RouterContextObj as RouterContext } from "./routing.js";
5
5
  export { mergeSearchString as _mergeSearchString } from "./utils.js";
6
6
  export * from "./data/index.js";
7
7
  export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs, RouteLoadFunc, RouteLoadFuncArgs, RouterResponseInit, CustomResponse } from "./types.js";
package/dist/index.js CHANGED
@@ -18,7 +18,14 @@ function createBeforeLeave() {
18
18
  preventDefault: () => e.defaultPrevented = true
19
19
  };
20
20
  for (const l of listeners) l.listener({
21
- ...e,
21
+ to,
22
+ options,
23
+ // delegate to the shared event so later listeners' preventDefault
24
+ // calls are observable from earlier listeners
25
+ get defaultPrevented() {
26
+ return e.defaultPrevented;
27
+ },
28
+ preventDefault: e.preventDefault,
22
29
  from: l.location,
23
30
  retry: force => {
24
31
  force && (ignore = true);
@@ -122,7 +129,12 @@ function createMatcher(path, partial, matchFilters) {
122
129
  const segments = pattern.split("/").filter(Boolean);
123
130
  const len = segments.length;
124
131
  return location => {
125
- const locSegments = location.split("/").filter(Boolean);
132
+ const locSegments = location.split("/");
133
+ // tolerate a single leading and trailing slash, but reject empty interior
134
+ // segments so `/foo//bar` doesn't silently match `/foo/bar` (#567)
135
+ if (locSegments[0] === "") locSegments.shift();
136
+ if (locSegments.length && locSegments[locSegments.length - 1] === "") locSegments.pop();
137
+ if (locSegments.includes("")) return null;
126
138
  const lenDiff = locSegments.length - len;
127
139
  if (lenDiff < 0 || lenDiff > 0 && splat === undefined && !partial) {
128
140
  return null;
@@ -364,7 +376,11 @@ const useMatch = (path, matchFilters) => {
364
376
  * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
365
377
  * ```
366
378
  */
367
- const useCurrentMatches = () => useRouter().matches;
379
+ const useCurrentMatches = () => {
380
+ const router = useRouter();
381
+ // return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
382
+ return () => router.matches().slice();
383
+ };
368
384
 
369
385
  /**
370
386
  * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
@@ -407,11 +423,20 @@ const useParams = () => useRouter().params;
407
423
  * ```
408
424
  */
409
425
  const useSearchParams = () => {
410
- const location = useLocation();
426
+ const router = useRouter();
427
+ const location = router.location;
411
428
  const navigate = useNavigate();
412
429
  const setSearchParams = (params, options) => {
413
- const searchString = untrack(() => mergeSearchString(location.search, params) + location.hash);
414
- navigate(searchString, {
430
+ const to = untrack(() => {
431
+ // merge onto the in-flight navigation target (if any) so consecutive
432
+ // synchronous calls compose instead of the later one winning
433
+ const pending = router.pendingTarget && new URL(router.pendingTarget.value, mockBase);
434
+ const pathname = pending ? pending.pathname : location.pathname;
435
+ const search = pending ? pending.search : location.search;
436
+ const hash = pending ? pending.hash : location.hash;
437
+ return pathname + mergeSearchString(search, params) + hash;
438
+ });
439
+ navigate(to, {
415
440
  scroll: false,
416
441
  resolve: false,
417
442
  ...options
@@ -455,6 +480,12 @@ const useBeforeLeave = listener => {
455
480
  });
456
481
  onCleanup(s);
457
482
  };
483
+
484
+ // Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
485
+ // pchar characters (sub-delims / ":" / "@") literal, matching how browsers
486
+ // report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
487
+ // still percent-encoded exactly as before, since browsers encode those too.
488
+ const encodeSegment = s => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
458
489
  function createRoutes(routeDef, base = "") {
459
490
  const {
460
491
  component,
@@ -475,7 +506,7 @@ function createRoutes(routeDef, base = "") {
475
506
  const path = joinPaths(base, expandedPath);
476
507
  let pattern = isLeaf ? path : path.split("/*", 1)[0];
477
508
  pattern = pattern.split("/").map(s => {
478
- return s.startsWith(":") || s.startsWith("*") ? s : encodeURIComponent(s);
509
+ return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
479
510
  }).join("/");
480
511
  acc.push({
481
512
  ...shared,
@@ -549,7 +580,9 @@ function createLocation(path, state, queryWrapper) {
549
580
  const url = createMemo(prev => {
550
581
  const path_ = path();
551
582
  try {
552
- return new URL(path_, origin);
583
+ // anchor rooted paths against the origin explicitly - a path with
584
+ // doubled leading slashes would otherwise parse as protocol-relative
585
+ return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
553
586
  } catch (err) {
554
587
  console.error(`Invalid path ${path_}`);
555
588
  return prev;
@@ -677,6 +710,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
677
710
  location,
678
711
  params,
679
712
  isRouting,
713
+ get pendingTarget() {
714
+ return lastTransitionTarget;
715
+ },
680
716
  renderPath,
681
717
  parsePath,
682
718
  navigatorFactory,
@@ -925,6 +961,9 @@ function Routes(props) {
925
961
  }
926
962
  const disposers = [];
927
963
  let root;
964
+ // dispose the detached per-route roots when this component unmounts, otherwise
965
+ // they stay subscribed to `matches` and crash on a later navigation (#451)
966
+ onCleanup(() => disposers.forEach(dispose => dispose()));
928
967
  const routeStates = createMemo(on(props.routerState.matches, (nextMatches, prevMatches, prev) => {
929
968
  let equal = prevMatches && nextMatches.length === prevMatches.length;
930
969
  const next = [];
@@ -1078,12 +1117,12 @@ function getCache() {
1078
1117
  * Revalidates the given cache entry/entries.
1079
1118
  */
1080
1119
  function revalidate(key, force = true) {
1120
+ const now = Date.now();
1121
+ // force the cache miss synchronously so a `refetch` in the same tick sees it —
1122
+ // startTransition defers its callback to a microtask (#497)
1123
+ force && cacheKeyOp(key, entry => entry[0] = 0);
1081
1124
  return startTransition(() => {
1082
- const now = Date.now();
1083
- cacheKeyOp(key, entry => {
1084
- force && (entry[0] = 0); //force cache miss
1085
- entry[4][1](now); // retrigger live signals
1086
- });
1125
+ cacheKeyOp(key, entry => entry[4][1](now)); // retrigger live signals
1087
1126
  });
1088
1127
  }
1089
1128
  function cacheKeyOp(key, fn) {
@@ -1265,7 +1304,7 @@ function useSubmission(fn, filter) {
1265
1304
  const submissions = useSubmissions(fn, filter);
1266
1305
  return new Proxy({}, {
1267
1306
  get(_, property) {
1268
- if (submissions.length === 0 && property === "clear" || property === "retry") return () => {};
1307
+ if (submissions.length === 0 && (property === "clear" || property === "retry")) return () => {};
1269
1308
  return submissions[submissions.length - 1]?.[property];
1270
1309
  }
1271
1310
  });
@@ -1356,7 +1395,10 @@ function toAction(fn, url) {
1356
1395
  fn.url = url;
1357
1396
  if (!isServer) {
1358
1397
  actions.set(url, fn);
1359
- getOwner() && onCleanup(() => actions.delete(url));
1398
+ // Only remove the registration if it still belongs to this instance —
1399
+ // a re-created action (e.g. a new `.with()` binding after revalidation)
1400
+ // may have registered itself under the same URL since.
1401
+ getOwner() && onCleanup(() => actions.get(url) === fn && actions.delete(url));
1360
1402
  }
1361
1403
  return fn;
1362
1404
  }
@@ -1511,7 +1553,7 @@ function setupNativeEvents({
1511
1553
  function Router(props) {
1512
1554
  if (isServer) return StaticRouter(props);
1513
1555
  const getSource = () => {
1514
- const url = window.location.pathname.replace(/^\/+/, "/") + window.location.search;
1556
+ const url = window.location.pathname + window.location.search;
1515
1557
  const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
1516
1558
  return {
1517
1559
  value: url + window.location.hash,
@@ -1681,8 +1723,9 @@ function A(props) {
1681
1723
  const isActive = createMemo(() => {
1682
1724
  const to_ = to();
1683
1725
  if (to_ === undefined) return [false, false];
1684
- const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
1685
- const loc = decodeURI(normalizePath(location.pathname).toLowerCase());
1726
+ // trailing slashes are ignored so `/route` and `/route/` share active state
1727
+ const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
1728
+ const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
1686
1729
  return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
1687
1730
  });
1688
1731
  return (() => {
package/dist/index.jsx CHANGED
@@ -1,6 +1,6 @@
1
1
  export * from "./routers/index.js";
2
2
  export * from "./components.jsx";
3
3
  export * from "./lifecycle.js";
4
- export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute } from "./routing.js";
4
+ export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute, RouterContextObj as RouterContext } from "./routing.js";
5
5
  export { mergeSearchString as _mergeSearchString } from "./utils.js";
6
6
  export * from "./data/index.js";
package/dist/lifecycle.js CHANGED
@@ -17,7 +17,14 @@ export function createBeforeLeave() {
17
17
  };
18
18
  for (const l of listeners)
19
19
  l.listener({
20
- ...e,
20
+ to,
21
+ options,
22
+ // delegate to the shared event so later listeners' preventDefault
23
+ // calls are observable from earlier listeners
24
+ get defaultPrevented() {
25
+ return e.defaultPrevented;
26
+ },
27
+ preventDefault: e.preventDefault,
21
28
  from: l.location,
22
29
  retry: (force) => {
23
30
  force && (ignore = true);
@@ -30,7 +30,7 @@ export function HashRouter(props) {
30
30
  saveCurrentDepth();
31
31
  },
32
32
  init: notify => bindEvent(window, "hashchange", notifyIfNotBlocked(notify, delta => !beforeLeave.confirm(delta && delta < 0 ? delta : getSource()))),
33
- create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
33
+ create: setupNativeEvents({ preload: props.preload, explicitLinks: props.explicitLinks, actionBase: props.actionBase }),
34
34
  utils: {
35
35
  go: delta => window.history.go(delta),
36
36
  renderPath: path => `#${path}`,
@@ -49,7 +49,7 @@ export function MemoryRouter(props) {
49
49
  get: memoryHistory.get,
50
50
  set: memoryHistory.set,
51
51
  init: memoryHistory.listen,
52
- create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
52
+ create: setupNativeEvents({ preload: props.preload, explicitLinks: props.explicitLinks, actionBase: props.actionBase }),
53
53
  utils: {
54
54
  go: memoryHistory.go
55
55
  }
@@ -7,7 +7,7 @@ export function Router(props) {
7
7
  if (isServer)
8
8
  return StaticRouter(props);
9
9
  const getSource = () => {
10
- const url = window.location.pathname.replace(/^\/+/, "/") + window.location.search;
10
+ const url = window.location.pathname + window.location.search;
11
11
  const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
12
12
  return {
13
13
  value: url + window.location.hash,
@@ -36,7 +36,7 @@ export function Router(props) {
36
36
  return !beforeLeave.confirm(s.value, { state: s.state });
37
37
  }
38
38
  })),
39
- create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase, props.transformUrl),
39
+ create: setupNativeEvents({ preload: props.preload, explicitLinks: props.explicitLinks, actionBase: props.actionBase, transformUrl: props.transformUrl }),
40
40
  utils: {
41
41
  go: delta => window.history.go(delta),
42
42
  beforeLeave
@@ -1,5 +1,5 @@
1
1
  import type { Component, JSX } from "solid-js";
2
- import type { MatchFilters, RouteDefinition, RoutePreloadFunc, RouterIntegration, RouteSectionProps } from "../types.js";
2
+ import type { MatchFilters, RouteDefinition, RoutePreloadFunc, RouterIntegration, RouteSectionComponent, RouteSectionProps } from "../types.js";
3
3
  export type BaseRouterProps = {
4
4
  base?: string;
5
5
  /**
@@ -19,7 +19,7 @@ export type RouteProps<S extends string, T = unknown> = {
19
19
  children?: JSX.Element;
20
20
  preload?: RoutePreloadFunc<T>;
21
21
  matchFilters?: MatchFilters<S>;
22
- component?: Component<RouteSectionProps<T>>;
22
+ component?: RouteSectionComponent<T>;
23
23
  info?: Record<string, any>;
24
24
  /** @deprecated use preload */
25
25
  load?: RoutePreloadFunc<T>;
@@ -1,5 +1,5 @@
1
1
  /*@refresh skip*/
2
- import { children, createMemo, createRoot, getOwner, mergeProps, on, Show, untrack } from "solid-js";
2
+ import { children, createMemo, createRoot, getOwner, mergeProps, on, onCleanup, Show, untrack } from "solid-js";
3
3
  import { getRequestEvent, isServer } from "solid-js/web";
4
4
  import { createBranches, createRouteContext, createRouterContext, getIntent, getRouteMatches, RouteContextObj, RouterContextObj, setInPreloadFn } from "../routing.js";
5
5
  export const createRouterComponent = (router) => (props) => {
@@ -54,6 +54,9 @@ function Routes(props) {
54
54
  }
55
55
  const disposers = [];
56
56
  let root;
57
+ // dispose the detached per-route roots when this component unmounts, otherwise
58
+ // they stay subscribed to `matches` and crash on a later navigation (#451)
59
+ onCleanup(() => disposers.forEach(dispose => dispose()));
57
60
  const routeStates = createMemo(on(props.routerState.matches, (nextMatches, prevMatches, prev) => {
58
61
  let equal = prevMatches && nextMatches.length === prevMatches.length;
59
62
  const next = [];
package/dist/routing.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { JSX, Accessor } from "solid-js";
2
2
  import type { BeforeLeaveEventArgs, Branch, Intent, Location, MatchFilters, NavigateOptions, Navigator, Params, RouteDescription, RouteContext, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SearchParams, SetSearchParams } from "./types.js";
3
+ /** Consider this API opaque and internal. It is likely to change in the future. */
3
4
  export declare const RouterContextObj: import("solid-js").Context<RouterContext | undefined>;
4
5
  export declare const RouteContextObj: import("solid-js").Context<RouteContext | undefined>;
5
6
  export declare const useRouter: () => RouterContext;
6
7
  export declare const useRoute: () => RouteContext;
7
8
  export declare const useResolvedPath: (path: () => string) => Accessor<string | undefined>;
8
- export declare const useHref: (to: () => string | undefined) => Accessor<string | undefined>;
9
+ export declare const useHref: <T extends string | undefined>(to: () => T) => () => string | T;
9
10
  /**
10
11
  * Retrieves method to do navigation. The method accepts a path to navigate to and an optional object with the following options:
11
12
  *
package/dist/routing.js CHANGED
@@ -4,6 +4,7 @@ import { isServer, getRequestEvent } from "solid-js/web";
4
4
  import { createBeforeLeave } from "./lifecycle.js";
5
5
  import { mockBase, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
6
6
  const MAX_REDIRECTS = 100;
7
+ /** Consider this API opaque and internal. It is likely to change in the future. */
7
8
  export const RouterContextObj = createContext();
8
9
  export const RouteContextObj = createContext();
9
10
  export const useRouter = () => invariant(useContext(RouterContextObj), "<A> and 'use' router primitives can be only used inside a Route.");
@@ -115,7 +116,11 @@ export const useMatch = (path, matchFilters) => {
115
116
  * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
116
117
  * ```
117
118
  */
118
- export const useCurrentMatches = () => useRouter().matches;
119
+ export const useCurrentMatches = () => {
120
+ const router = useRouter();
121
+ // return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
122
+ return () => router.matches().slice();
123
+ };
119
124
  /**
120
125
  * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
121
126
  *
@@ -156,11 +161,20 @@ export const useParams = () => useRouter().params;
156
161
  * ```
157
162
  */
158
163
  export const useSearchParams = () => {
159
- const location = useLocation();
164
+ const router = useRouter();
165
+ const location = router.location;
160
166
  const navigate = useNavigate();
161
167
  const setSearchParams = (params, options) => {
162
- const searchString = untrack(() => mergeSearchString(location.search, params) + location.hash);
163
- navigate(searchString, {
168
+ const to = untrack(() => {
169
+ // merge onto the in-flight navigation target (if any) so consecutive
170
+ // synchronous calls compose instead of the later one winning
171
+ const pending = router.pendingTarget && new URL(router.pendingTarget.value, mockBase);
172
+ const pathname = pending ? pending.pathname : location.pathname;
173
+ const search = pending ? pending.search : location.search;
174
+ const hash = pending ? pending.hash : location.hash;
175
+ return pathname + mergeSearchString(search, params) + hash;
176
+ });
177
+ navigate(to, {
164
178
  scroll: false,
165
179
  resolve: false,
166
180
  ...options
@@ -203,6 +217,11 @@ export const useBeforeLeave = (listener) => {
203
217
  });
204
218
  onCleanup(s);
205
219
  };
220
+ // Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
221
+ // pchar characters (sub-delims / ":" / "@") literal, matching how browsers
222
+ // report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
223
+ // still percent-encoded exactly as before, since browsers encode those too.
224
+ const encodeSegment = (s) => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
206
225
  export function createRoutes(routeDef, base = "") {
207
226
  const { component, preload, load, children, info } = routeDef;
208
227
  const isLeaf = !children || (Array.isArray(children) && !children.length);
@@ -219,7 +238,7 @@ export function createRoutes(routeDef, base = "") {
219
238
  pattern = pattern
220
239
  .split("/")
221
240
  .map((s) => {
222
- return s.startsWith(":") || s.startsWith("*") ? s : encodeURIComponent(s);
241
+ return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
223
242
  })
224
243
  .join("/");
225
244
  acc.push({
@@ -295,7 +314,9 @@ function createLocation(path, state, queryWrapper) {
295
314
  const url = createMemo(prev => {
296
315
  const path_ = path();
297
316
  try {
298
- return new URL(path_, origin);
317
+ // anchor rooted paths against the origin explicitly - a path with
318
+ // doubled leading slashes would otherwise parse as protocol-relative
319
+ return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
299
320
  }
300
321
  catch (err) {
301
322
  console.error(`Invalid path ${path_}`);
@@ -420,6 +441,9 @@ export function createRouterContext(integration, branches, getContext, options =
420
441
  location,
421
442
  params,
422
443
  isRouting,
444
+ get pendingTarget() {
445
+ return lastTransitionTarget;
446
+ },
423
447
  renderPath,
424
448
  parsePath,
425
449
  navigatorFactory,
package/dist/types.d.ts CHANGED
@@ -42,7 +42,7 @@ export interface NavigateOptions<S = unknown> {
42
42
  state: S;
43
43
  }
44
44
  export interface Navigator {
45
- (to: string, options?: Partial<NavigateOptions>): void;
45
+ (to: string | number, options?: Partial<NavigateOptions>): void;
46
46
  (delta: number): void;
47
47
  }
48
48
  export type NavigatorFactory = (route?: RouteContext) => Navigator;
@@ -71,12 +71,13 @@ export interface RouteSectionProps<T = unknown> {
71
71
  data: T;
72
72
  children?: JSX.Element;
73
73
  }
74
- export type RouteDefinition<S extends string | string[] = any, T = unknown> = {
74
+ export type RouteSectionComponent<T = unknown> = Component<RouteSectionProps<T>> | Component<Omit<RouteSectionProps<T>, "children">> | Component<{}>;
75
+ export type RouteDefinition<S extends string | string[] = any, T = any> = {
75
76
  path?: S;
76
77
  matchFilters?: MatchFilters<S>;
77
78
  preload?: RoutePreloadFunc<T>;
78
79
  children?: RouteDefinition | RouteDefinition[];
79
- component?: Component<RouteSectionProps<T>>;
80
+ component?: RouteSectionComponent<T>;
80
81
  info?: Record<string, any>;
81
82
  /** @deprecated use preload */
82
83
  load?: RoutePreloadFunc;
@@ -137,6 +138,8 @@ export interface RouterContext {
137
138
  params: Params;
138
139
  navigatorFactory: NavigatorFactory;
139
140
  isRouting: () => boolean;
141
+ /** The target of the in-flight navigation transition, if any. Not reactive. */
142
+ readonly pendingTarget?: LocationChange;
140
143
  matches: () => RouteMatch[];
141
144
  renderPath(path: string): string;
142
145
  parsePath(str: string): string;
package/dist/utils.d.ts CHANGED
@@ -10,3 +10,4 @@ export declare function scoreRoute(route: RouteDescription): number;
10
10
  export declare function createMemoObject<T extends Record<string | symbol, unknown>>(fn: () => T): T;
11
11
  export declare function mergeSearchString(search: string, params: SetSearchParams): string;
12
12
  export declare function expandOptionals(pattern: string): string[];
13
+ export declare function setFunctionName<T>(obj: T, value: string): T;
package/dist/utils.js CHANGED
@@ -52,7 +52,15 @@ export function createMatcher(path, partial, matchFilters) {
52
52
  const segments = pattern.split("/").filter(Boolean);
53
53
  const len = segments.length;
54
54
  return (location) => {
55
- const locSegments = location.split("/").filter(Boolean);
55
+ const locSegments = location.split("/");
56
+ // tolerate a single leading and trailing slash, but reject empty interior
57
+ // segments so `/foo//bar` doesn't silently match `/foo/bar` (#567)
58
+ if (locSegments[0] === "")
59
+ locSegments.shift();
60
+ if (locSegments.length && locSegments[locSegments.length - 1] === "")
61
+ locSegments.pop();
62
+ if (locSegments.includes(""))
63
+ return null;
56
64
  const lenDiff = locSegments.length - len;
57
65
  if (lenDiff < 0 || (lenDiff > 0 && splat === undefined && !partial)) {
58
66
  return null;
@@ -175,3 +183,11 @@ export function expandOptionals(pattern) {
175
183
  }
176
184
  return expandOptionals(suffix).reduce((results, expansion) => [...results, ...prefixes.map(p => p + expansion)], []);
177
185
  }
186
+ export function setFunctionName(obj, value) {
187
+ Object.defineProperty(obj, "name", {
188
+ value,
189
+ writable: false,
190
+ configurable: false
191
+ });
192
+ return obj;
193
+ }
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.16.0",
9
+ "version": "0.16.2",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -51,7 +51,7 @@
51
51
  "solid-js": "^1.8.6"
52
52
  },
53
53
  "scripts": {
54
- "build": "tsc && rollup -c",
54
+ "build": "rm -rf dist && tsc && rollup -c",
55
55
  "test": "vitest run && npm run test:types",
56
56
  "test:watch": "vitest",
57
57
  "test:types": "tsc --project tsconfig.test.json",
@@ -1,31 +0,0 @@
1
- import type { JSX } from "solid-js";
2
- import type { Location, Navigator } from "./types.js";
3
- declare module "solid-js" {
4
- namespace JSX {
5
- interface AnchorHTMLAttributes<T> {
6
- state?: string;
7
- noScroll?: boolean;
8
- replace?: boolean;
9
- preload?: boolean;
10
- link?: boolean;
11
- }
12
- }
13
- }
14
- export interface AnchorProps extends Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "state"> {
15
- href: string;
16
- replace?: boolean | undefined;
17
- noScroll?: boolean | undefined;
18
- state?: unknown | undefined;
19
- inactiveClass?: string | undefined;
20
- activeClass?: string | undefined;
21
- end?: boolean | undefined;
22
- }
23
- export declare function A(props: AnchorProps): JSX.Element;
24
- export interface NavigateProps {
25
- href: ((args: {
26
- navigate: Navigator;
27
- location: Location;
28
- }) => string) | string;
29
- state?: unknown;
30
- }
31
- export declare function Navigate(props: NavigateProps): null;