@solidjs/router 2.0.0-next.33 → 2.0.0-next.35

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/routing.d.ts CHANGED
@@ -165,9 +165,17 @@ export declare function useSearchParams<T extends SearchParams>(): [
165
165
  (params: SetSearchParams, options?: Partial<NavigateOptions>) => void
166
166
  ];
167
167
  export interface LinkState {
168
- /** The location matches this link or lives under it (exact-only when `end`). Styling: `data-active`. */
168
+ /**
169
+ * The location's pathname matches this link's or lives under it; the query
170
+ * is ignored. A link to the router's root (`/`, under the router's `base`)
171
+ * is exact-only, as is every link with `end`.
172
+ * Styling: `data-active`.
173
+ */
169
174
  active: () => boolean;
170
- /** The location matches this link exactly — what `aria-current="page"` reflects. */
175
+ /**
176
+ * The location matches this link exactly: same pathname and same query,
177
+ * parameter order and hash aside — what `aria-current="page"` reflects.
178
+ */
171
179
  current: () => boolean;
172
180
  /** This link is the target of an in-flight navigation. Styling: `data-pending`. */
173
181
  pending: () => boolean;
package/dist/routing.js CHANGED
@@ -4,7 +4,7 @@ import { runWithOwner } from "solid-js";
4
4
  import { DEV } from "solid-js";
5
5
  import { createComponent, createContext, createMemo, createSignal, getOwner, isPending, latest, NotReadyError, untrack, useContext } from "solid-js";
6
6
  import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
7
- import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals, validateSearch } from "./utils.js";
7
+ import { mockBase, createMemoObject, extractSearchParams, invariant, matchLink, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals, validateSearch } from "./utils.js";
8
8
  import { HREF } from "./paths.js";
9
9
  import { serverRouteOf, serverRouteArgs, serverRouteArgsEqual } from "./serverRouteShared.js";
10
10
  const MAX_REDIRECTS = 100;
@@ -227,22 +227,11 @@ export const useLinkState = (href, options = {}) => {
227
227
  const router = useRouter();
228
228
  const location = router.location;
229
229
  const to = useResolvedPath(() => String(href()));
230
- // trailing slashes are ignored so `/route` and `/route/` share state
231
- const path = createMemo(() => {
232
- const to_ = to();
233
- return to_ === undefined ? undefined : comparablePath(to_);
234
- });
235
- const matches = (loc) => {
236
- const path_ = path();
237
- if (path_ === undefined)
238
- return [false, false];
239
- const exact = loc === path_;
240
- return [exact || (!options.end && loc.startsWith(path_ + "/")), exact];
241
- };
242
- const state = createMemo(() => matches(decodeURI(comparablePath(location.pathname))));
230
+ const base = router.base.path();
231
+ const state = createMemo(() => matchLink(location, to(), base, options.end));
243
232
  return {
244
- active: () => state()[0],
245
- current: () => state()[1],
233
+ active: createMemo(() => state().active),
234
+ current: createMemo(() => state().current),
246
235
  // match the in-flight target explicitly (rather than active-while-routing)
247
236
  // so the answer is the same from pure reads and from effects, which
248
237
  // observe the committed location during a transition
@@ -250,7 +239,8 @@ export const useLinkState = (href, options = {}) => {
250
239
  state(); // location dependency: mid-flight target swaps recompute
251
240
  return (router.isRouting() &&
252
241
  !!router.pendingTarget &&
253
- matches(decodeURI(comparablePath(router.pendingTarget.value)))[0]);
242
+ matchLink({ pathname: router.pendingTarget.value, search: "" }, to(), base, options.end)
243
+ .active);
254
244
  })
255
245
  };
256
246
  };
@@ -556,6 +546,7 @@ let flashDecoder;
556
546
  export function provideFlashDecoder(decoder) {
557
547
  flashDecoder || (flashDecoder = decoder);
558
548
  }
549
+ const flashStates = new WeakMap();
559
550
  let preloadIntent;
560
551
  export function getIntent() {
561
552
  return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
@@ -592,56 +583,60 @@ export function createRouterContext(integration, branches, getContext, options =
592
583
  // unread outcome must not haunt a later request's render. Only detection
593
584
  // and clearing happen here (the runtime's isomorphic half); the raw header
594
585
  // is stashed and decoding waits for the action-provided codec, read when
595
- // the lazily allocated submissions signal below first initializes.
596
- let flashCookieHeader;
586
+ // the lazily allocated submissions signal below first initializes. Both
587
+ // happen once per request: a router re-created for the same request finds
588
+ // the request's flash state (see `flashStates`) and neither clears again
589
+ // nor decodes again.
590
+ let flash;
597
591
  if (isServer) {
598
592
  const e = getRequestEvent();
599
593
  if (e && !(e.router && e.router.submission)) {
600
- const cookieHeader = e.request.headers.get("cookie");
601
- if (hasFlashCookie(cookieHeader)) {
602
- flashCookieHeader = cookieHeader;
603
- // one-shot: clear it even when unreadable so it can't haunt later renders
604
- if (e.response && e.response.headers)
605
- e.response.headers.append("Set-Cookie", clearFlashCookie());
594
+ flash = flashStates.get(e);
595
+ if (!flash) {
596
+ const cookieHeader = e.request.headers.get("cookie");
597
+ if (hasFlashCookie(cookieHeader)) {
598
+ flashStates.set(e, (flash = { cookieHeader }));
599
+ // one-shot: clear it even when unreadable so it can't haunt later renders
600
+ if (e.response && e.response.headers)
601
+ e.response.headers.append("Set-Cookie", clearFlashCookie());
602
+ }
606
603
  }
607
604
  }
608
605
  }
609
- // The decode, at most once per request: the decoder may answer with a
610
- // Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
611
- // async), and this cache is what keeps the parked read's rerun from
612
- // restarting it — resumption finds the settled outcome and just reads it.
613
- // A decoder that rejects reads as "no flash", matching the runtime's own
614
- // malformed-cookie semantics.
615
- let flashDecode;
616
606
  // The seeding read, as a memo: NotReadyError must surface from a reactive
617
607
  // node the graph can park and retry — never from router setup, which no
618
- // boundary guards — and the memo bounds the recompute to this function;
619
- // a parked reader resumes into the settled cache above, never a second
620
- // decode. Created only when a flash cookie actually arrived (server-only
621
- // by construction: flashCookieHeader is only ever set there), and
608
+ // boundary guards — and the memo bounds the recompute to this function.
609
+ // The decode runs at most once per request: the decoder may answer with a
610
+ // Promise (the cookie is encrypted; the runtime's decodeFlashCookie is
611
+ // async), and the request's cached state is what keeps a parked read's
612
+ // rerun — or a re-created router's first read — from restarting it. A
613
+ // decoder that rejects reads as "no flash", matching the runtime's own
614
+ // malformed-cookie semantics. Created only when a flash cookie actually
615
+ // arrived (server-only by construction: `flash` is only ever set there), and
622
616
  // - `lazy`: server memos compute eagerly by default — deferred to first
623
617
  // read, a request whose submissions are never read never decodes;
624
618
  // - `transparent`: the memo exists on the server only, so its owner
625
619
  // must not consume a hydration-id slot — the client, which seeds
626
620
  // submissions as [] without ever creating this memo, would miss it
627
621
  // and every sibling id would shift.
628
- const flashSubmission = flashCookieHeader !== undefined
622
+ const flashSubmission = flash !== undefined
629
623
  ? createMemo(() => {
624
+ const state = flash;
630
625
  if (!flashDecoder)
631
626
  return undefined;
632
- if (!flashDecode) {
633
- const promise = flashDecoder(flashCookieHeader).then(value => {
634
- flashDecode = { done: true, value };
627
+ if (!state.decode) {
628
+ const promise = flashDecoder(state.cookieHeader).then(value => {
629
+ state.decode = { done: true, value };
635
630
  }, () => {
636
- flashDecode = { done: true, value: undefined };
631
+ state.decode = { done: true, value: undefined };
637
632
  });
638
- flashDecode = { done: false, promise };
633
+ state.decode = { done: false, promise };
639
634
  }
640
635
  // SSR carries the Promise through NotReadyError so the parked
641
636
  // reader can resume, exactly like the lazy matches above.
642
- if (!flashDecode.done)
643
- throw new NotReadyError(flashDecode.promise);
644
- return flashDecode.value;
637
+ if (!state.decode.done)
638
+ throw new NotReadyError(state.decode.promise);
639
+ return state.decode.value;
645
640
  }, { lazy: true, transparent: true })
646
641
  : undefined;
647
642
  let submissions;
package/dist/utils.d.ts CHANGED
@@ -1,8 +1,36 @@
1
1
  import type { StandardSchemaV1, MatchFilters, PathMatch, RouteDescription, SearchParams, SetSearchParams } from "./types.js";
2
2
  export declare const mockBase = "http://sr";
3
3
  export declare function normalizePath(path: string, omitSlash?: boolean): string;
4
- /** Pathname stripped of search/hash and trailing slash, lowercased — the form link matching compares. */
4
+ /**
5
+ * Pathname stripped of search/hash and trailing slash, percent-encoded the
6
+ * way `URL` (and so `location.pathname`) reports it, lowercased — the form
7
+ * link matching compares. Raw and encoded spellings (`/café`, `/caf%C3%A9`)
8
+ * meet; escapes are never decoded, so `%2F` is not a path separator.
9
+ */
5
10
  export declare const comparablePath: (path: string) => string;
11
+ /**
12
+ * Whether a URL pathname is the router's base path or under it, on a segment
13
+ * boundary: base `/app` covers `/app`, `/app/` and `/app/x`, not `/apple`.
14
+ * Case-insensitive; no base (`""` or `/`) covers every path.
15
+ */
16
+ export declare function isUnderBase(pathname: string, base: string): boolean;
17
+ /**
18
+ * The link-state rule shared by claimed anchors and `useLinkState`, given
19
+ * the location and a link's already-resolved target (path, optional query
20
+ * and hash):
21
+ *
22
+ * - `current` — same pathname and same query (parameter order and hash aside)
23
+ * - `active` — same pathname or one under it; the router's root (`base`, or
24
+ * `/` without one) only matches exactly, since it is a prefix of every
25
+ * page. `end` makes every link exact-only.
26
+ */
27
+ export declare function matchLink(location: {
28
+ pathname: string;
29
+ search: string;
30
+ }, target: string | undefined, base: string, end?: boolean): {
31
+ active: boolean;
32
+ current: boolean;
33
+ };
6
34
  export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
7
35
  export declare function invariant<T>(value: T | null | undefined, message: string): T;
8
36
  export declare function joinPaths(from: string, to: string): string;
package/dist/utils.js CHANGED
@@ -6,8 +6,54 @@ export function normalizePath(path, omitSlash = false) {
6
6
  const s = path.replace(trimPathRegex, "$1");
7
7
  return s ? (omitSlash || /^[?#]/.test(s) ? s : "/" + s) : "";
8
8
  }
9
- /** Pathname stripped of search/hash and trailing slash, lowercased — the form link matching compares. */
10
- export const comparablePath = (path) => normalizePath(path.split(/[?#]/, 1)[0]).toLowerCase().replace(/\/$/, "");
9
+ /**
10
+ * Pathname stripped of search/hash and trailing slash, percent-encoded the
11
+ * way `URL` (and so `location.pathname`) reports it, lowercased — the form
12
+ * link matching compares. Raw and encoded spellings (`/café`, `/caf%C3%A9`)
13
+ * meet; escapes are never decoded, so `%2F` is not a path separator.
14
+ */
15
+ export const comparablePath = (path) => new URL(mockBase + normalizePath(path.split(/[?#]/, 1)[0])).pathname
16
+ .toLowerCase()
17
+ .replace(/\/$/, "");
18
+ /**
19
+ * Whether a URL pathname is the router's base path or under it, on a segment
20
+ * boundary: base `/app` covers `/app`, `/app/` and `/app/x`, not `/apple`.
21
+ * Case-insensitive; no base (`""` or `/`) covers every path.
22
+ */
23
+ export function isUnderBase(pathname, base) {
24
+ const b = base.toLowerCase().replace(/\/+$/, "");
25
+ const p = pathname.toLowerCase();
26
+ return !b || !p || p === b || p.startsWith(b + "/");
27
+ }
28
+ /** A query string as an order-independent comparable string. */
29
+ const comparableQuery = (search) => {
30
+ const params = new URLSearchParams(search);
31
+ params.sort();
32
+ return params.toString();
33
+ };
34
+ /**
35
+ * The link-state rule shared by claimed anchors and `useLinkState`, given
36
+ * the location and a link's already-resolved target (path, optional query
37
+ * and hash):
38
+ *
39
+ * - `current` — same pathname and same query (parameter order and hash aside)
40
+ * - `active` — same pathname or one under it; the router's root (`base`, or
41
+ * `/` without one) only matches exactly, since it is a prefix of every
42
+ * page. `end` makes every link exact-only.
43
+ */
44
+ export function matchLink(location, target, base, end) {
45
+ if (target === undefined)
46
+ return { active: false, current: false };
47
+ const loc = comparablePath(location.pathname);
48
+ const path = comparablePath(target);
49
+ const exact = loc === path;
50
+ const hashless = target.split("#", 1)[0];
51
+ const q = hashless.indexOf("?");
52
+ return {
53
+ active: exact || (!end && path !== "" && path !== comparablePath(base) && loc.startsWith(path + "/")),
54
+ current: exact && comparableQuery(location.search) === comparableQuery(q < 0 ? "" : hashless.slice(q))
55
+ };
56
+ }
11
57
  export function resolvePath(base, path, from) {
12
58
  if (hasSchemeRegex.test(path)) {
13
59
  return undefined;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.33",
9
+ "version": "2.0.0-next.35",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",