@solidjs/router 0.17.0-next.3 → 0.17.0-next.5

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.
@@ -1,6 +1,6 @@
1
- import type { JSX } from "solid-js";
1
+ import type { JSX } from "@solidjs/web";
2
2
  import type { Location, Navigator } from "./types.js";
3
- declare module "solid-js" {
3
+ declare module "@solidjs/web/jsx-runtime" {
4
4
  namespace JSX {
5
5
  interface AnchorHTMLAttributes<T> {
6
6
  state?: string;
@@ -26,8 +26,9 @@ export function A(props) {
26
26
  const to_ = to();
27
27
  if (to_ === undefined)
28
28
  return [false, false];
29
- const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
30
- const loc = decodeURI(normalizePath(location.pathname).toLowerCase());
29
+ // trailing slashes are ignored so `/route` and `/route/` share active state
30
+ const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
31
+ const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
31
32
  return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
32
33
  });
33
34
  const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass]
@@ -1,4 +1,4 @@
1
- import { JSX } from "solid-js";
1
+ import { type JSX } from "@solidjs/web";
2
2
  import type { Submission, NarrowResponse } from "../types.js";
3
3
  export type Action<T extends Array<any>, U, V = T> = (T extends [FormData | URLSearchParams] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
4
4
  url: string;
@@ -1,5 +1,6 @@
1
1
  import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
2
- import { isServer } from "@solidjs/web";
2
+ import { isResponseEnvelope, isServer } from "@solidjs/web";
3
+ import { decodeResponse } from "@solidjs/web/server-functions";
3
4
  import { useRouter } from "../routing.js";
4
5
  import { mockBase, setFunctionName } from "../utils.js";
5
6
  import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
@@ -122,7 +123,11 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
122
123
  fn[invokeSymbol] = invoke;
123
124
  if (!isServer) {
124
125
  actions.set(url, fn);
125
- getOwner() && onCleanup(() => actions.delete(url));
126
+ // Only remove the registration if it still belongs to this instance —
127
+ // a re-created action (e.g. a new `.with()` binding after revalidation)
128
+ // may have registered itself under the same URL since.
129
+ getOwner() &&
130
+ onCleanup(() => actions.get(url) === fn && actions.delete(url));
126
131
  }
127
132
  return fn;
128
133
  }
@@ -147,19 +152,35 @@ async function handleResponse(response, error, navigate) {
147
152
  let custom;
148
153
  let keys;
149
154
  let flightKeys;
150
- if (response instanceof Response) {
151
- if (response.headers.has("X-Revalidate"))
152
- keys = response.headers.get("X-Revalidate").split(",");
153
- if (response.customBody) {
154
- data = custom = await response.customBody();
155
+ let metadata;
156
+ if (isResponseEnvelope(response)) {
157
+ // client-only respond(): the value rides in memory beside the metadata
158
+ data = response.value;
159
+ metadata = response.response;
160
+ }
161
+ else if (response instanceof Response) {
162
+ metadata = response;
163
+ // responses the transport hands over whole (redirects, revalidation,
164
+ // single-flight) carry a codec-encoded body the router decodes itself
165
+ if (response.body) {
166
+ data = await decodeResponse(response);
155
167
  if (response.headers.has("X-Single-Flight")) {
156
- data = data._$value;
168
+ custom = data;
169
+ data = custom._$value;
157
170
  delete custom._$value;
158
171
  flightKeys = Object.keys(custom);
159
172
  }
160
173
  }
161
- if (response.headers.has("Location")) {
162
- const locationUrl = response.headers.get("Location") || "/";
174
+ }
175
+ else if (error)
176
+ return { error: response };
177
+ else
178
+ data = response;
179
+ if (metadata) {
180
+ if (metadata.headers.has("X-Revalidate"))
181
+ keys = metadata.headers.get("X-Revalidate").split(",");
182
+ if (metadata.headers.has("Location")) {
183
+ const locationUrl = metadata.headers.get("Location") || "/";
163
184
  if (locationUrl.startsWith("http")) {
164
185
  window.location.href = locationUrl;
165
186
  }
@@ -168,10 +189,6 @@ async function handleResponse(response, error, navigate) {
168
189
  }
169
190
  }
170
191
  }
171
- else if (error)
172
- return { error: response };
173
- else
174
- data = response;
175
192
  // invalidate
176
193
  cacheKeyOp(keys, entry => (entry[0] = 0));
177
194
  // set cache
@@ -1,3 +1,2 @@
1
1
  export { action, useSubmissions, useAction, type Action } from "./action.js";
2
2
  export { query, revalidate, type CachedFunction } from "./query.js";
3
- export { redirect, reload, json } from "./response.js";
@@ -1,3 +1,2 @@
1
1
  export { action, useSubmissions, useAction } from "./action.js";
2
2
  export { query, revalidate } from "./query.js";
3
- export { redirect, reload, json } from "./response.js";
@@ -1,5 +1,6 @@
1
1
  import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js";
2
- import { getRequestEvent, isServer } from "@solidjs/web";
2
+ import { getRequestEvent, isResponseEnvelope, isServer } from "@solidjs/web";
3
+ import { decodeResponse } from "@solidjs/web/server-functions";
3
4
  import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
4
5
  const LocationHeader = "Location";
5
6
  const PRELOAD_TIMEOUT = 5000;
@@ -143,6 +144,14 @@ export function query(fn, name) {
143
144
  return res;
144
145
  function handleResponse(error) {
145
146
  return async (v) => {
147
+ let enveloped;
148
+ let hasEnveloped = false;
149
+ if (isResponseEnvelope(v)) {
150
+ // respond(): the value rides in memory beside the metadata
151
+ enveloped = v.value;
152
+ hasEnveloped = true;
153
+ v = v.response;
154
+ }
146
155
  if (v instanceof Response) {
147
156
  const e = getRequestEvent();
148
157
  if (e) {
@@ -164,8 +173,15 @@ export function query(fn, name) {
164
173
  e.response.status = 302;
165
174
  return;
166
175
  }
167
- if (v.customBody)
168
- v = await v.customBody();
176
+ if (hasEnveloped)
177
+ v = enveloped;
178
+ else if (v.body) {
179
+ // responses the transport hands over whole (revalidation) carry a
180
+ // codec-encoded body; anything else (a raw user Response) stays whole
181
+ const decoded = await decodeResponse(v);
182
+ if (decoded !== undefined)
183
+ v = decoded;
184
+ }
169
185
  }
170
186
  if (error)
171
187
  throw v;
package/dist/index.d.ts CHANGED
@@ -4,4 +4,4 @@ export * from "./lifecycle.js";
4
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
- export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs, RouterResponseInit, CustomResponse } from "./types.js";
7
+ export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs } from "./types.js";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps, template } from '@solidjs/web';
1
+ import { isServer, getRequestEvent, createComponent as createComponent$1, memo, isResponseEnvelope, delegateEvents, spread, mergeProps, template } from '@solidjs/web';
2
2
  import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, flush, createComponent, children, merge, createRoot, sharedConfig, getObserver, $TRACK, action as action$1, omit } from 'solid-js';
3
+ import { decodeResponse } from '@solidjs/web/server-functions';
3
4
 
4
5
  function createBeforeLeave() {
5
6
  let listeners = new Set();
@@ -17,7 +18,14 @@ function createBeforeLeave() {
17
18
  preventDefault: () => e.defaultPrevented = true
18
19
  };
19
20
  for (const l of listeners) l.listener({
20
- ...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,
21
29
  from: l.location,
22
30
  retry: force => {
23
31
  force && (ignore = true);
@@ -121,7 +129,12 @@ function createMatcher(path, partial, matchFilters) {
121
129
  const segments = pattern.split("/").filter(Boolean);
122
130
  const len = segments.length;
123
131
  return location => {
124
- 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;
125
138
  const lenDiff = locSegments.length - len;
126
139
  if (lenDiff < 0 || lenDiff > 0 && splat === undefined && !partial) {
127
140
  return null;
@@ -370,7 +383,11 @@ const useMatch = (path, matchFilters) => {
370
383
  * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
371
384
  * ```
372
385
  */
373
- const useCurrentMatches = () => useRouter().matches;
386
+ const useCurrentMatches = () => {
387
+ const router = useRouter();
388
+ // return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
389
+ return () => router.matches().slice();
390
+ };
374
391
 
375
392
  /**
376
393
  * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
@@ -383,7 +400,7 @@ const useCurrentMatches = () => useRouter().matches;
383
400
  * const getUser = query(() => fetchUser(params.id), "user");
384
401
  * ```
385
402
  */
386
- const useParams = () => useRouter().params;
403
+ const useParams = () => useRoute().params;
387
404
 
388
405
  /**
389
406
  * Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
@@ -413,11 +430,20 @@ const useParams = () => useRouter().params;
413
430
  * ```
414
431
  */
415
432
  const useSearchParams = () => {
416
- const location = useLocation();
433
+ const router = useRouter();
434
+ const location = router.location;
417
435
  const navigate = useNavigate();
418
436
  const setSearchParams = (params, options) => {
419
- const searchString = untrack(() => mergeSearchString(location.search, params) + location.hash);
420
- navigate(searchString, {
437
+ const to = untrack(() => {
438
+ // merge onto the in-flight navigation target (if any) so consecutive
439
+ // synchronous calls compose instead of the later one winning
440
+ const pending = router.pendingTarget && new URL(router.pendingTarget.value, mockBase);
441
+ const pathname = pending ? pending.pathname : location.pathname;
442
+ const search = pending ? pending.search : location.search;
443
+ const hash = pending ? pending.hash : location.hash;
444
+ return pathname + mergeSearchString(search, params) + hash;
445
+ });
446
+ navigate(to, {
421
447
  scroll: false,
422
448
  resolve: false,
423
449
  ...options
@@ -461,6 +487,12 @@ const useBeforeLeave = listener => {
461
487
  });
462
488
  onCleanup(s);
463
489
  };
490
+
491
+ // Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
492
+ // pchar characters (sub-delims / ":" / "@") literal, matching how browsers
493
+ // report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
494
+ // still percent-encoded exactly as before, since browsers encode those too.
495
+ const encodeSegment = s => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
464
496
  function createRoutes(routeDef, base = "") {
465
497
  const {
466
498
  component,
@@ -480,7 +512,7 @@ function createRoutes(routeDef, base = "") {
480
512
  const path = joinPaths(base, expandedPath);
481
513
  let pattern = isLeaf ? path : path.split("/*", 1)[0];
482
514
  pattern = pattern.split("/").map(s => {
483
- return s.startsWith(":") || s.startsWith("*") ? s : encodeURIComponent(s);
515
+ return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
484
516
  }).join("/");
485
517
  acc.push({
486
518
  ...shared,
@@ -549,12 +581,21 @@ function getRouteMatches(branches, location) {
549
581
  }
550
582
  return [];
551
583
  }
584
+ function mergeParams(matches) {
585
+ const params = {};
586
+ for (let i = 0; i < matches.length; i++) {
587
+ Object.assign(params, matches[i].params);
588
+ }
589
+ return params;
590
+ }
552
591
  function createLocation(path, state, queryWrapper) {
553
592
  const origin = new URL(mockBase);
554
593
  const url = createMemo((prev = origin) => {
555
594
  const path_ = path();
556
595
  try {
557
- return new URL(path_, origin);
596
+ // anchor rooted paths against the origin explicitly - a path with
597
+ // doubled leading slashes would otherwise parse as protocol-relative
598
+ return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
558
599
  } catch (err) {
559
600
  console.error(`Invalid path ${path_}`);
560
601
  return prev;
@@ -642,17 +683,12 @@ function createRouterContext(integration, branches, getContext, options = {}) {
642
683
  }
643
684
  return getRouteMatches(branches(), location.pathname);
644
685
  });
645
- const buildParams = () => {
646
- const m = matches();
647
- const params = {};
648
- for (let i = 0; i < m.length; i++) {
649
- Object.assign(params, m[i].params);
650
- }
651
- return params;
652
- };
653
- const params = utils.paramsWrapper ? utils.paramsWrapper(buildParams, branches) : createMemoObject(buildParams);
686
+ const buildParams = () => mergeParams(matches());
687
+ const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
688
+ const params = wrapParams(buildParams);
654
689
  const baseRoute = {
655
690
  pattern: basePath,
691
+ params,
656
692
  path: () => basePath,
657
693
  outlet: () => null,
658
694
  resolvePath(to) {
@@ -663,7 +699,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
663
699
  base: baseRoute,
664
700
  location,
665
701
  params,
702
+ wrapParams,
666
703
  isRouting,
704
+ get pendingTarget() {
705
+ return lastTransitionTarget;
706
+ },
667
707
  renderPath,
668
708
  parsePath,
669
709
  navigatorFactory,
@@ -808,11 +848,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
808
848
  }];
809
849
  }
810
850
  }
811
- function createRouteContext(router, parent, outlet, match) {
851
+ function createRouteContext(router, parent, outlet, match, matches = () => [match()]) {
812
852
  const {
813
853
  base,
814
854
  location,
815
- params
855
+ wrapParams
816
856
  } = router;
817
857
  const {
818
858
  pattern,
@@ -820,6 +860,10 @@ function createRouteContext(router, parent, outlet, match) {
820
860
  preload
821
861
  } = match().route;
822
862
  const path = createMemo(() => match().path);
863
+ // Params scoped to this route's lifetime. `matches` is expected to retain
864
+ // its last valid value while this route is being torn down, so outgoing
865
+ // components and preloads never observe another route's params.
866
+ const params = wrapParams(() => mergeParams(matches()));
823
867
  component && component.preload && component.preload();
824
868
  inPreloadFn = true;
825
869
  const data = preload ? preload({
@@ -831,6 +875,7 @@ function createRouteContext(router, parent, outlet, match) {
831
875
  const route = {
832
876
  parent,
833
877
  pattern,
878
+ params,
834
879
  path,
835
880
  outlet: () => component ? createComponent(component, {
836
881
  params,
@@ -940,6 +985,13 @@ function Routes(props) {
940
985
  const disposers = [];
941
986
  let root;
942
987
  let prevMatches;
988
+ // dispose the detached per-route roots when this component unmounts, otherwise
989
+ // they stay subscribed to `matches` and crash on a later navigation (#451)
990
+ onCleanup(() => disposers.forEach(dispose => dispose()));
991
+ // Route roots must outlive re-runs of the `routeStates` memo below, so they
992
+ // are created under the owner of this component rather than the memo's
993
+ // computation (which disposes its children every time it re-runs).
994
+ const owner = getOwner();
943
995
  const routeStates = createMemo(prev => {
944
996
  const nextMatches = props.routerState.matches();
945
997
  const previousMatches = prevMatches;
@@ -955,13 +1007,19 @@ function Routes(props) {
955
1007
  if (disposers[i]) {
956
1008
  disposers[i]();
957
1009
  }
958
- createRoot(dispose => {
1010
+ runWithOwner(owner, () => createRoot(dispose => {
959
1011
  disposers[i] = dispose;
960
- next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => {
1012
+ const routeKey = nextMatch.route.key;
1013
+ // Retain the last matches in which this route participated so
1014
+ // that its components and preloads never observe another
1015
+ // route's params/path while this route is being torn down.
1016
+ const matchesAtLevel = createMemo(prev => {
961
1017
  const routeMatches = props.routerState.matches();
962
- return routeMatches[i] ?? routeMatches[0];
1018
+ const m = routeMatches[i];
1019
+ return m && m.route.key === routeKey ? routeMatches : prev || nextMatches;
963
1020
  });
964
- });
1021
+ next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => matchesAtLevel()[i], matchesAtLevel);
1022
+ }));
965
1023
  }
966
1024
  }
967
1025
  disposers.splice(nextMatches.length).forEach(dispose => dispose());
@@ -1193,6 +1251,14 @@ function query(fn, name) {
1193
1251
  return res;
1194
1252
  function handleResponse(error) {
1195
1253
  return async v => {
1254
+ let enveloped;
1255
+ let hasEnveloped = false;
1256
+ if (isResponseEnvelope(v)) {
1257
+ // respond(): the value rides in memory beside the metadata
1258
+ enveloped = v.value;
1259
+ hasEnveloped = true;
1260
+ v = v.response;
1261
+ }
1196
1262
  if (v instanceof Response) {
1197
1263
  const e = getRequestEvent();
1198
1264
  if (e) {
@@ -1208,7 +1274,12 @@ function query(fn, name) {
1208
1274
  });else if (!isServer) window.location.href = url;else if (e) e.response.status = 302;
1209
1275
  return;
1210
1276
  }
1211
- if (v.customBody) v = await v.customBody();
1277
+ if (hasEnveloped) v = enveloped;else if (v.body) {
1278
+ // responses the transport hands over whole (revalidation) carry a
1279
+ // codec-encoded body; anything else (a raw user Response) stays whole
1280
+ const decoded = await decodeResponse(v);
1281
+ if (decoded !== undefined) v = decoded;
1282
+ }
1212
1283
  }
1213
1284
  if (error) throw v;
1214
1285
  cached[2] = v;
@@ -1386,7 +1457,10 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
1386
1457
  fn[invokeSymbol] = invoke;
1387
1458
  if (!isServer) {
1388
1459
  actions.set(url, fn);
1389
- getOwner() && onCleanup(() => actions.delete(url));
1460
+ // Only remove the registration if it still belongs to this instance —
1461
+ // a re-created action (e.g. a new `.with()` binding after revalidation)
1462
+ // may have registered itself under the same URL since.
1463
+ getOwner() && onCleanup(() => actions.get(url) === fn && actions.delete(url));
1390
1464
  }
1391
1465
  return fn;
1392
1466
  }
@@ -1411,27 +1485,38 @@ async function handleResponse(response, error, navigate) {
1411
1485
  let custom;
1412
1486
  let keys;
1413
1487
  let flightKeys;
1414
- if (response instanceof Response) {
1415
- if (response.headers.has("X-Revalidate")) keys = response.headers.get("X-Revalidate").split(",");
1416
- if (response.customBody) {
1417
- data = custom = await response.customBody();
1488
+ let metadata;
1489
+ if (isResponseEnvelope(response)) {
1490
+ // client-only respond(): the value rides in memory beside the metadata
1491
+ data = response.value;
1492
+ metadata = response.response;
1493
+ } else if (response instanceof Response) {
1494
+ metadata = response;
1495
+ // responses the transport hands over whole (redirects, revalidation,
1496
+ // single-flight) carry a codec-encoded body the router decodes itself
1497
+ if (response.body) {
1498
+ data = await decodeResponse(response);
1418
1499
  if (response.headers.has("X-Single-Flight")) {
1419
- data = data._$value;
1500
+ custom = data;
1501
+ data = custom._$value;
1420
1502
  delete custom._$value;
1421
1503
  flightKeys = Object.keys(custom);
1422
1504
  }
1423
1505
  }
1424
- if (response.headers.has("Location")) {
1425
- const locationUrl = response.headers.get("Location") || "/";
1506
+ } else if (error) return {
1507
+ error: response
1508
+ };else data = response;
1509
+ if (metadata) {
1510
+ if (metadata.headers.has("X-Revalidate")) keys = metadata.headers.get("X-Revalidate").split(",");
1511
+ if (metadata.headers.has("Location")) {
1512
+ const locationUrl = metadata.headers.get("Location") || "/";
1426
1513
  if (locationUrl.startsWith("http")) {
1427
1514
  window.location.href = locationUrl;
1428
1515
  } else {
1429
1516
  navigate(locationUrl);
1430
1517
  }
1431
1518
  }
1432
- } else if (error) return {
1433
- error: response
1434
- };else data = response;
1519
+ }
1435
1520
  // invalidate
1436
1521
  cacheKeyOp(keys, entry => entry[0] = 0);
1437
1522
  // set cache
@@ -1556,7 +1641,7 @@ function setupNativeEvents({
1556
1641
  function Router(props) {
1557
1642
  if (isServer) return StaticRouter(props);
1558
1643
  const getSource = () => {
1559
- const url = window.location.pathname.replace(/^\/+/, "/") + window.location.search;
1644
+ const url = window.location.pathname + window.location.search;
1560
1645
  const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
1561
1646
  return {
1562
1647
  value: url + window.location.hash,
@@ -1714,6 +1799,9 @@ function MemoryRouter(props) {
1714
1799
  }
1715
1800
 
1716
1801
  var _tmpl$ = /*#__PURE__*/template(`<a>`);
1802
+
1803
+ // the JSX namespace lives in @solidjs/web's jsx-runtime module as of 2.0
1804
+
1717
1805
  function toClassName(value) {
1718
1806
  if (!value) return "";
1719
1807
  if (typeof value === "string" || typeof value === "number") return String(value);
@@ -1735,30 +1823,29 @@ function A(props) {
1735
1823
  const isActive = createMemo(() => {
1736
1824
  const to_ = to();
1737
1825
  if (to_ === undefined) return [false, false];
1738
- const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
1739
- const loc = decodeURI(normalizePath(location.pathname).toLowerCase());
1826
+ // trailing slashes are ignored so `/route` and `/route/` share active state
1827
+ const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
1828
+ const loc = decodeURI(normalizePath(location.pathname).toLowerCase().replace(/\/$/, ""));
1740
1829
  return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
1741
1830
  });
1742
1831
  const className = createMemo(() => [toClassName(props.class), isActive()[0] ? props.activeClass : props.inactiveClass].filter(Boolean).join(" "));
1743
- return (() => {
1744
- var _el$ = _tmpl$();
1745
- spread(_el$, mergeProps(rest, {
1746
- get href() {
1747
- return href() || props.href;
1748
- },
1749
- get state() {
1750
- return JSON.stringify(props.state);
1751
- },
1752
- get ["class"]() {
1753
- return className();
1754
- },
1755
- "link": true,
1756
- get ["aria-current"]() {
1757
- return isActive()[1] ? "page" : undefined;
1758
- }
1759
- }), false);
1760
- return _el$;
1761
- })();
1832
+ var _el$ = _tmpl$();
1833
+ spread(_el$, mergeProps(rest, {
1834
+ get href() {
1835
+ return href() || props.href;
1836
+ },
1837
+ get state() {
1838
+ return JSON.stringify(props.state);
1839
+ },
1840
+ get ["class"]() {
1841
+ return className();
1842
+ },
1843
+ "link": true,
1844
+ get ["aria-current"]() {
1845
+ return isActive()[1] ? "page" : undefined;
1846
+ }
1847
+ }), false);
1848
+ return _el$;
1762
1849
  }
1763
1850
  function Navigate(props) {
1764
1851
  const navigate = useNavigate();
@@ -1778,57 +1865,4 @@ function Navigate(props) {
1778
1865
  return null;
1779
1866
  }
1780
1867
 
1781
- function redirect(url, init = 302) {
1782
- let responseInit;
1783
- let revalidate;
1784
- if (typeof init === "number") {
1785
- responseInit = {
1786
- status: init
1787
- };
1788
- } else {
1789
- ({
1790
- revalidate,
1791
- ...responseInit
1792
- } = init);
1793
- if (typeof responseInit.status === "undefined") {
1794
- responseInit.status = 302;
1795
- }
1796
- }
1797
- const headers = new Headers(responseInit.headers);
1798
- headers.set("Location", url);
1799
- revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
1800
- const response = new Response(null, {
1801
- ...responseInit,
1802
- headers: headers
1803
- });
1804
- return response;
1805
- }
1806
- function reload(init = {}) {
1807
- const {
1808
- revalidate,
1809
- ...responseInit
1810
- } = init;
1811
- const headers = new Headers(responseInit.headers);
1812
- revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
1813
- return new Response(null, {
1814
- ...responseInit,
1815
- headers
1816
- });
1817
- }
1818
- function json(data, init = {}) {
1819
- const {
1820
- revalidate,
1821
- ...responseInit
1822
- } = init;
1823
- const headers = new Headers(responseInit.headers);
1824
- revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
1825
- headers.set("Content-Type", "application/json");
1826
- const response = new Response(JSON.stringify(data), {
1827
- ...responseInit,
1828
- headers
1829
- });
1830
- response.customBody = () => data;
1831
- return response;
1832
- }
1833
-
1834
- export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmissions };
1868
+ export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, createBeforeLeave, createMemoryHistory, createRouter, keepDepth, notifyIfNotBlocked, query, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmissions };
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);
@@ -1,4 +1,4 @@
1
- import type { JSX } from "solid-js";
1
+ import type { JSX } from "@solidjs/web";
2
2
  import type { BaseRouterProps } from "./components.jsx";
3
3
  export declare function hashParser(str: string): string;
4
4
  export type HashRouterProps = BaseRouterProps & {
@@ -1,6 +1,6 @@
1
1
  import type { LocationChange } from "../types.js";
2
2
  import type { BaseRouterProps } from "./components.jsx";
3
- import type { JSX } from "solid-js";
3
+ import type { JSX } from "@solidjs/web";
4
4
  export type MemoryHistory = {
5
5
  get: () => string;
6
6
  set: (change: LocationChange) => void;
@@ -1,5 +1,5 @@
1
1
  import type { BaseRouterProps } from "./components.jsx";
2
- import type { JSX } from "solid-js";
2
+ import type { JSX } from "@solidjs/web";
3
3
  export type RouterProps = BaseRouterProps & {
4
4
  url?: string;
5
5
  actionBase?: string;
@@ -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,
@@ -1,5 +1,5 @@
1
1
  import { type BaseRouterProps } from "./components.jsx";
2
- import type { JSX } from "solid-js";
2
+ import type { JSX } from "@solidjs/web";
3
3
  export type StaticRouterProps = BaseRouterProps & {
4
4
  url?: string;
5
5
  };
@@ -1,5 +1,6 @@
1
- import type { Component, JSX } from "solid-js";
2
- import type { MatchFilters, RouteDefinition, RoutePreloadFunc, RouterIntegration, RouteSectionProps } from "../types.js";
1
+ import type { Component } from "solid-js";
2
+ import type { JSX } from "@solidjs/web";
3
+ import type { MatchFilters, RouteDefinition, RoutePreloadFunc, RouterIntegration, RouteSectionComponent, RouteSectionProps } from "../types.js";
3
4
  export type BaseRouterProps = {
4
5
  base?: string;
5
6
  /**
@@ -17,7 +18,7 @@ export type RouteProps<S extends string, T = unknown> = {
17
18
  children?: JSX.Element;
18
19
  preload?: RoutePreloadFunc<T>;
19
20
  matchFilters?: MatchFilters<S>;
20
- component?: Component<RouteSectionProps<T>>;
21
+ component?: RouteSectionComponent<T>;
21
22
  info?: Record<string, any>;
22
23
  };
23
24
  export declare const Route: <S extends string, T = unknown>(props: RouteProps<S, T>) => JSX.Element;
@@ -1,5 +1,5 @@
1
1
  /*@refresh skip*/
2
- import { children, createMemo, createRoot, getOwner, merge, untrack } from "solid-js";
2
+ import { children, createMemo, createRoot, getOwner, merge, onCleanup, runWithOwner, untrack } from "solid-js";
3
3
  import { getRequestEvent, isServer } from "@solidjs/web";
4
4
  import { createBranches, createRouteContext, createRouterContext, getIntent, getRouteMatches, RouteContextObj, RouterContextObj, setInPreloadFn } from "../routing.js";
5
5
  export const createRouterComponent = (router) => function IntegratedRouter(props) {
@@ -64,6 +64,13 @@ function Routes(props) {
64
64
  const disposers = [];
65
65
  let root;
66
66
  let prevMatches;
67
+ // dispose the detached per-route roots when this component unmounts, otherwise
68
+ // they stay subscribed to `matches` and crash on a later navigation (#451)
69
+ onCleanup(() => disposers.forEach(dispose => dispose()));
70
+ // Route roots must outlive re-runs of the `routeStates` memo below, so they
71
+ // are created under the owner of this component rather than the memo's
72
+ // computation (which disposes its children every time it re-runs).
73
+ const owner = getOwner();
67
74
  const routeStates = createMemo((prev) => {
68
75
  const nextMatches = props.routerState.matches();
69
76
  const previousMatches = prevMatches;
@@ -80,13 +87,19 @@ function Routes(props) {
80
87
  if (disposers[i]) {
81
88
  disposers[i]();
82
89
  }
83
- createRoot(dispose => {
90
+ runWithOwner(owner, () => createRoot(dispose => {
84
91
  disposers[i] = dispose;
85
- next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => {
92
+ const routeKey = nextMatch.route.key;
93
+ // Retain the last matches in which this route participated so
94
+ // that its components and preloads never observe another
95
+ // route's params/path while this route is being torn down.
96
+ const matchesAtLevel = createMemo((prev) => {
86
97
  const routeMatches = props.routerState.matches();
87
- return routeMatches[i] ?? routeMatches[0];
98
+ const m = routeMatches[i];
99
+ return m && m.route.key === routeKey ? routeMatches : prev || nextMatches;
88
100
  });
89
- });
101
+ next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()?.[i + 1]), () => matchesAtLevel()[i], matchesAtLevel);
102
+ }));
90
103
  }
91
104
  }
92
105
  disposers.splice(nextMatches.length).forEach(dispose => dispose());
@@ -5,6 +5,6 @@ export declare function createRouter(config: {
5
5
  init?: (notify: (value?: string | LocationChange) => void) => () => void;
6
6
  create?: (router: RouterContext) => void;
7
7
  utils?: Partial<RouterUtils>;
8
- }): (props: import("./components.jsx").BaseRouterProps) => import("solid-js").JSX.Element;
8
+ }): (props: import("./components.jsx").BaseRouterProps) => import("@solidjs/web").JSX.Element;
9
9
  export declare function bindEvent(target: EventTarget, type: string, handler: EventListener): () => void;
10
10
  export declare function scrollToHash(hash: string, fallbackTop?: boolean): void;
package/dist/routing.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { JSX, Accessor } from "solid-js";
1
+ import type { JSX } from "@solidjs/web";
2
2
  import type { BeforeLeaveEventArgs, Branch, Intent, Location, MatchFilters, NavigateOptions, Navigator, Params, RouteDescription, RouteContext, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SearchParams, SetSearchParams } from "./types.js";
3
3
  /** Consider this API opaque and internal. It is likely to change in the future. */
4
- export declare const RouterContextObj: import("solid-js").Context<RouterContext | undefined>;
5
- export declare const RouteContextObj: import("solid-js").Context<RouteContext | undefined>;
4
+ export declare const RouterContextObj: import("solid-js").Context<RouterContext>;
5
+ export declare const RouteContextObj: import("solid-js").Context<RouteContext>;
6
6
  export declare const useRouter: () => RouterContext;
7
7
  export declare const useRoute: () => RouteContext;
8
- export declare const useResolvedPath: (path: () => string) => Accessor<string | undefined>;
8
+ export declare const useResolvedPath: (path: () => string) => import("solid-js").SourceAccessor<string | undefined>;
9
9
  export declare const useHref: <T extends string | undefined>(to: () => T) => () => string | T;
10
10
  /**
11
11
  * Retrieves method to do navigation. The method accepts a path to navigate to and an optional object with the following options:
@@ -79,7 +79,7 @@ export declare const usePreloadRoute: () => (url: string | URL, options?: {
79
79
  * return <div classList={{ active: Boolean(match()) }} />;
80
80
  * ```
81
81
  */
82
- export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S>) => Accessor<import("./types.js").PathMatch | undefined>;
82
+ export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S>) => import("solid-js").SourceAccessor<import("./types.js").PathMatch | undefined>;
83
83
  /**
84
84
  * `useCurrentMatches` returns all the matches for the current matched route.
85
85
  * Useful for getting all the route information.
@@ -172,4 +172,4 @@ export declare function createRouterContext(integration: RouterIntegration, bran
172
172
  singleFlight?: boolean;
173
173
  transformUrl?: (url: string) => string;
174
174
  }): RouterContext;
175
- export declare function createRouteContext(router: RouterContext, parent: RouteContext, outlet: () => JSX.Element, match: () => RouteMatch): RouteContext;
175
+ export declare function createRouteContext(router: RouterContext, parent: RouteContext, outlet: () => JSX.Element, match: () => RouteMatch, matches?: () => RouteMatch[]): RouteContext;
package/dist/routing.js CHANGED
@@ -124,7 +124,11 @@ export const useMatch = (path, matchFilters) => {
124
124
  * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
125
125
  * ```
126
126
  */
127
- export const useCurrentMatches = () => useRouter().matches;
127
+ export const useCurrentMatches = () => {
128
+ const router = useRouter();
129
+ // return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
130
+ return () => router.matches().slice();
131
+ };
128
132
  /**
129
133
  * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
130
134
  *
@@ -136,7 +140,7 @@ export const useCurrentMatches = () => useRouter().matches;
136
140
  * const getUser = query(() => fetchUser(params.id), "user");
137
141
  * ```
138
142
  */
139
- export const useParams = () => useRouter().params;
143
+ export const useParams = () => useRoute().params;
140
144
  /**
141
145
  * Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
142
146
  * The object is a proxy so you must access properties to subscribe to reactive updates.
@@ -165,11 +169,20 @@ export const useParams = () => useRouter().params;
165
169
  * ```
166
170
  */
167
171
  export const useSearchParams = () => {
168
- const location = useLocation();
172
+ const router = useRouter();
173
+ const location = router.location;
169
174
  const navigate = useNavigate();
170
175
  const setSearchParams = (params, options) => {
171
- const searchString = untrack(() => mergeSearchString(location.search, params) + location.hash);
172
- navigate(searchString, {
176
+ const to = untrack(() => {
177
+ // merge onto the in-flight navigation target (if any) so consecutive
178
+ // synchronous calls compose instead of the later one winning
179
+ const pending = router.pendingTarget && new URL(router.pendingTarget.value, mockBase);
180
+ const pathname = pending ? pending.pathname : location.pathname;
181
+ const search = pending ? pending.search : location.search;
182
+ const hash = pending ? pending.hash : location.hash;
183
+ return pathname + mergeSearchString(search, params) + hash;
184
+ });
185
+ navigate(to, {
173
186
  scroll: false,
174
187
  resolve: false,
175
188
  ...options
@@ -212,6 +225,11 @@ export const useBeforeLeave = (listener) => {
212
225
  });
213
226
  onCleanup(s);
214
227
  };
228
+ // Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
229
+ // pchar characters (sub-delims / ":" / "@") literal, matching how browsers
230
+ // report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
231
+ // still percent-encoded exactly as before, since browsers encode those too.
232
+ const encodeSegment = (s) => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
215
233
  export function createRoutes(routeDef, base = "") {
216
234
  const { component, preload, children, info } = routeDef;
217
235
  const isLeaf = !children || (Array.isArray(children) && !children.length);
@@ -228,7 +246,7 @@ export function createRoutes(routeDef, base = "") {
228
246
  pattern = pattern
229
247
  .split("/")
230
248
  .map((s) => {
231
- return s.startsWith(":") || s.startsWith("*") ? s : encodeURIComponent(s);
249
+ return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
232
250
  })
233
251
  .join("/");
234
252
  acc.push({
@@ -299,12 +317,21 @@ export function getRouteMatches(branches, location) {
299
317
  }
300
318
  return [];
301
319
  }
320
+ function mergeParams(matches) {
321
+ const params = {};
322
+ for (let i = 0; i < matches.length; i++) {
323
+ Object.assign(params, matches[i].params);
324
+ }
325
+ return params;
326
+ }
302
327
  function createLocation(path, state, queryWrapper) {
303
328
  const origin = new URL(mockBase);
304
329
  const url = createMemo((prev = origin) => {
305
330
  const path_ = path();
306
331
  try {
307
- return new URL(path_, origin);
332
+ // anchor rooted paths against the origin explicitly - a path with
333
+ // doubled leading slashes would otherwise parse as protocol-relative
334
+ return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
308
335
  }
309
336
  catch (err) {
310
337
  console.error(`Invalid path ${path_}`);
@@ -382,19 +409,14 @@ export function createRouterContext(integration, branches, getContext, options =
382
409
  }
383
410
  return getRouteMatches(branches(), location.pathname);
384
411
  });
385
- const buildParams = () => {
386
- const m = matches();
387
- const params = {};
388
- for (let i = 0; i < m.length; i++) {
389
- Object.assign(params, m[i].params);
390
- }
391
- return params;
392
- };
393
- const params = utils.paramsWrapper
394
- ? utils.paramsWrapper(buildParams, branches)
395
- : createMemoObject(buildParams);
412
+ const buildParams = () => mergeParams(matches());
413
+ const wrapParams = utils.paramsWrapper
414
+ ? (getParams) => utils.paramsWrapper(getParams, branches)
415
+ : (getParams) => createMemoObject(getParams);
416
+ const params = wrapParams(buildParams);
396
417
  const baseRoute = {
397
418
  pattern: basePath,
419
+ params,
398
420
  path: () => basePath,
399
421
  outlet: () => null,
400
422
  resolvePath(to) {
@@ -405,7 +427,11 @@ export function createRouterContext(integration, branches, getContext, options =
405
427
  base: baseRoute,
406
428
  location,
407
429
  params,
430
+ wrapParams,
408
431
  isRouting,
432
+ get pendingTarget() {
433
+ return lastTransitionTarget;
434
+ },
409
435
  renderPath,
410
436
  parsePath,
411
437
  navigatorFactory,
@@ -539,10 +565,14 @@ export function createRouterContext(integration, branches, getContext, options =
539
565
  ];
540
566
  }
541
567
  }
542
- export function createRouteContext(router, parent, outlet, match) {
543
- const { base, location, params } = router;
568
+ export function createRouteContext(router, parent, outlet, match, matches = () => [match()]) {
569
+ const { base, location, wrapParams } = router;
544
570
  const { pattern, component, preload } = match().route;
545
571
  const path = createMemo(() => match().path);
572
+ // Params scoped to this route's lifetime. `matches` is expected to retain
573
+ // its last valid value while this route is being torn down, so outgoing
574
+ // components and preloads never observe another route's params.
575
+ const params = wrapParams(() => mergeParams(matches()));
546
576
  component &&
547
577
  component.preload &&
548
578
  component.preload();
@@ -552,6 +582,7 @@ export function createRouteContext(router, parent, outlet, match) {
552
582
  const route = {
553
583
  parent,
554
584
  pattern,
585
+ params,
555
586
  path,
556
587
  outlet: () => component
557
588
  ? createComponent(component, {
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { Component, JSX, Signal } from "solid-js";
1
+ import type { Component, Signal } from "solid-js";
2
+ import type { JSX, ResponseEnvelope } from "@solidjs/web";
2
3
  declare module "@solidjs/web" {
3
4
  interface RequestEvent {
4
5
  response: {
@@ -55,7 +56,7 @@ export interface LocationChange<S = unknown> {
55
56
  rawPath?: string;
56
57
  }
57
58
  export interface RouterIntegration {
58
- signal: Signal<LocationChange>;
59
+ signal: [get: () => LocationChange, set: (next: LocationChange) => void];
59
60
  create?: (router: RouterContext) => void;
60
61
  utils?: Partial<RouterUtils>;
61
62
  }
@@ -72,12 +73,13 @@ export interface RouteSectionProps<T = unknown> {
72
73
  data: T;
73
74
  children?: JSX.Element;
74
75
  }
75
- export type RouteDefinition<S extends string | string[] = any, T = unknown> = {
76
+ export type RouteSectionComponent<T = unknown> = Component<RouteSectionProps<T>> | Component<Omit<RouteSectionProps<T>, "children">> | Component<{}>;
77
+ export type RouteDefinition<S extends string | string[] = any, T = any> = {
76
78
  path?: S;
77
79
  matchFilters?: MatchFilters<S>;
78
80
  preload?: RoutePreloadFunc<T>;
79
81
  children?: RouteDefinition | RouteDefinition[];
80
- component?: Component<RouteSectionProps<T>>;
82
+ component?: RouteSectionComponent<T>;
81
83
  info?: Record<string, any>;
82
84
  };
83
85
  export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean);
@@ -118,6 +120,7 @@ export interface RouteContext {
118
120
  parent?: RouteContext;
119
121
  child?: RouteContext;
120
122
  pattern: string;
123
+ params: Params;
121
124
  path: () => string;
122
125
  outlet: () => JSX.Element;
123
126
  resolvePath(to: string): string | undefined;
@@ -134,8 +137,11 @@ export interface RouterContext {
134
137
  base: RouteContext;
135
138
  location: Location;
136
139
  params: Params;
140
+ wrapParams: (getParams: () => Params) => Params;
137
141
  navigatorFactory: NavigatorFactory;
138
142
  isRouting: () => boolean;
143
+ /** The target of the in-flight navigation transition, if any. Not reactive. */
144
+ readonly pendingTarget?: LocationChange;
139
145
  matches: () => RouteMatch[];
140
146
  renderPath(path: string): string;
141
147
  parsePath(str: string): string;
@@ -175,11 +181,4 @@ export interface MaybePreloadableComponent extends Component {
175
181
  export type CacheEntry = [number, Promise<any>, any, Intent | undefined, Signal<number> & {
176
182
  count: number;
177
183
  }];
178
- export type NarrowResponse<T> = T extends CustomResponse<infer U> ? U : Exclude<T, Response>;
179
- export type RouterResponseInit = Omit<ResponseInit, "body"> & {
180
- revalidate?: string | string[];
181
- };
182
- export type CustomResponse<T> = Omit<Response, "clone"> & {
183
- customBody: () => T;
184
- clone(...args: readonly unknown[]): CustomResponse<T>;
185
- };
184
+ export type NarrowResponse<T> = T extends ResponseEnvelope<infer U> ? U : Exclude<T, Response>;
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;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.17.0-next.3",
9
+ "version": "0.17.0-next.5",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "devDependencies": {
32
- "@solidjs/web": "2.0.0-beta.7",
32
+ "@solidjs/web": "2.0.0-beta.19",
33
33
  "@babel/core": "^7.26.0",
34
34
  "@babel/preset-typescript": "^7.26.0",
35
35
  "@changesets/cli": "^2.27.10",
@@ -38,19 +38,19 @@
38
38
  "@rollup/plugin-terser": "0.4.4",
39
39
  "@types/jest": "^29.5.14",
40
40
  "@types/node": "^22.10.0",
41
- "babel-preset-solid": "2.0.0-beta.7",
41
+ "babel-preset-solid": "2.0.0-beta.19",
42
42
  "jsdom": "^25.0.1",
43
43
  "prettier": "^3.4.1",
44
44
  "rollup": "^4.27.4",
45
- "solid-js": "2.0.0-beta.7",
45
+ "solid-js": "2.0.0-beta.19",
46
46
  "typescript": "^5.7.2",
47
47
  "vite": "^6.0.0",
48
- "vite-plugin-solid": "3.0.0-next.4",
48
+ "vite-plugin-solid": "3.0.0-next.10",
49
49
  "vitest": "^2.1.6"
50
50
  },
51
51
  "peerDependencies": {
52
- "@solidjs/web": ">=2.0.0-beta.7 <2.0.0-experimental.0",
53
- "solid-js": ">=2.0.0-beta.7 <2.0.0-experimental.0"
52
+ "@solidjs/web": ">=2.0.0-beta.19 <2.0.0-experimental.0",
53
+ "solid-js": ">=2.0.0-beta.19 <2.0.0-experimental.0"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "rm -rf dist && tsc && rollup -c",
@@ -1,4 +0,0 @@
1
- import type { RouterResponseInit, CustomResponse } from "../types.js";
2
- export declare function redirect(url: string, init?: number | RouterResponseInit): CustomResponse<never>;
3
- export declare function reload(init?: RouterResponseInit): CustomResponse<never>;
4
- export declare function json<T>(data: T, init?: RouterResponseInit): CustomResponse<T>;
@@ -1,42 +0,0 @@
1
- export function redirect(url, init = 302) {
2
- let responseInit;
3
- let revalidate;
4
- if (typeof init === "number") {
5
- responseInit = { status: init };
6
- }
7
- else {
8
- ({ revalidate, ...responseInit } = init);
9
- if (typeof responseInit.status === "undefined") {
10
- responseInit.status = 302;
11
- }
12
- }
13
- const headers = new Headers(responseInit.headers);
14
- headers.set("Location", url);
15
- revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
16
- const response = new Response(null, {
17
- ...responseInit,
18
- headers: headers
19
- });
20
- return response;
21
- }
22
- export function reload(init = {}) {
23
- const { revalidate, ...responseInit } = init;
24
- const headers = new Headers(responseInit.headers);
25
- revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
26
- return new Response(null, {
27
- ...responseInit,
28
- headers
29
- });
30
- }
31
- export function json(data, init = {}) {
32
- const { revalidate, ...responseInit } = init;
33
- const headers = new Headers(responseInit.headers);
34
- revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
35
- headers.set("Content-Type", "application/json");
36
- const response = new Response(JSON.stringify(data), {
37
- ...responseInit,
38
- headers
39
- });
40
- response.customBody = () => data;
41
- return response;
42
- }