@tschk/moonshine-router 0.3.5 → 0.3.7

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 (2) hide show
  1. package/package.json +3 -3
  2. package/src/navigation.ts +27 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tschk/moonshine-router",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Renderer-neutral route graph and matcher with optional React navigation",
5
5
  "license": "ISC",
6
6
  "type": "module",
@@ -16,8 +16,8 @@
16
16
  "test": "bun test"
17
17
  },
18
18
  "dependencies": {
19
- "@tschk/moonshine": "^0.3.5",
20
- "@tschk/moonshine-framework": "^0.3.5"
19
+ "@tschk/moonshine": "^0.3.6",
20
+ "@tschk/moonshine-framework": "^0.3.6"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/bun": "1.3.14",
package/src/navigation.ts CHANGED
@@ -15,17 +15,36 @@ import { createRouteGraph, matchRoutes, type RouteMatch } from "./graph.js";
15
15
 
16
16
  export type RouteParams = Record<string, string>;
17
17
 
18
+ /**
19
+ * The location the signal carries: pathname plus query.
20
+ *
21
+ * The query has to be in the signal, not read off `window.location` at render
22
+ * time. A navigation that changes only the query would otherwise write the
23
+ * same pathname back, notify no subscriber, and leave every `useSearchParams`
24
+ * caller rendering the previous query — a tab bar driven by `?section=` would
25
+ * change the URL and not the page. The hash stays out: it is still pushed to
26
+ * history, but these hooks do not read it and including it would corrupt the
27
+ * query when consumers split on the first "?".
28
+ */
18
29
  function getBrowserPath(): string {
19
30
  if (typeof window === "undefined") return "/";
20
- return window.location.pathname || "/";
31
+ return (window.location.pathname || "/") + window.location.search;
32
+ }
33
+
34
+ /** Splits `"/a/b?x=1"` into its pathname and its raw query string. */
35
+ export function splitLocation(location: string): [string, string] {
36
+ const index = location.indexOf("?");
37
+ return index === -1
38
+ ? [location, ""]
39
+ : [location.slice(0, index), location.slice(index + 1)];
21
40
  }
22
41
 
23
42
  export type MoonshineRouterInstance = {
24
43
  /** Navigate via History API (no full reload). */
25
44
  navigate: (to: string, options?: { replace?: boolean }) => void;
26
- /** Current pathname (reactive signal). */
45
+ /** Current pathname and query (reactive signal). */
27
46
  getLocation: () => string;
28
- /** Subscribe-friendly location signal. */
47
+ /** Subscribe-friendly location signal, carrying pathname and query. */
29
48
  location: Signal<string>;
30
49
  /** Sync signal from `window.location`. */
31
50
  syncFromBrowser: () => void;
@@ -52,7 +71,7 @@ export function createMoonshineRouter(
52
71
  } else {
53
72
  window.history.pushState({}, "", url.pathname + url.search + url.hash);
54
73
  }
55
- location.set(url.pathname);
74
+ location.set(url.pathname + url.search);
56
75
  };
57
76
 
58
77
  return {
@@ -77,7 +96,7 @@ export function navigate(to: string, options?: { replace?: boolean }): void {
77
96
  currentRouter().navigate(to, options);
78
97
  }
79
98
 
80
- /** Current pathname (reactive; active or fallback router). */
99
+ /** Current pathname and query (reactive; active or fallback router). */
81
100
  export function getLocation(): string {
82
101
  return currentRouter().getLocation();
83
102
  }
@@ -158,7 +177,9 @@ export function MoonshineRouter(props: MoonshineRouterProps): ReactNode {
158
177
  () => runtime.location.peek(),
159
178
  () => props.path ?? "/",
160
179
  );
161
- const pathname = props.path ?? browserPath;
180
+ // Routes are matched on the pathname alone; the query rides along in the
181
+ // signal only so that changing it re-renders.
182
+ const pathname = splitLocation(props.path ?? browserPath)[0];
162
183
  const [ready, setReady] = useState(
163
184
  typeof window === "undefined" || props.path !== undefined,
164
185
  );