sv-router 0.7.1 → 0.8.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sv-router",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Modern Svelte Routing",
5
5
  "keywords": [
6
6
  "svelte",
@@ -36,28 +36,29 @@
36
36
  "esm-env": "^1.2.2"
37
37
  },
38
38
  "devDependencies": {
39
- "@changesets/cli": "^2.29.4",
40
- "@eslint/js": "^9.28.0",
41
- "@sveltejs/vite-plugin-svelte": "^5.1.0",
42
- "@testing-library/jest-dom": "^6.6.3",
39
+ "@changesets/cli": "^2.29.5",
40
+ "@eslint/js": "^9.32.0",
41
+ "@sveltejs/vite-plugin-svelte": "^6.1.0",
42
+ "@testing-library/jest-dom": "^6.6.4",
43
43
  "@testing-library/svelte": "^5.2.8",
44
- "@types/node": "^22.15.30",
45
- "eslint": "^9.28.0",
46
- "eslint-config-prettier": "^10.1.5",
44
+ "@testing-library/user-event": "^14.6.1",
45
+ "@types/node": "^24.2.0",
46
+ "eslint": "^9.32.0",
47
+ "eslint-config-prettier": "^10.1.8",
47
48
  "eslint-plugin-simple-import-sort": "^12.1.1",
48
- "eslint-plugin-svelte": "^3.9.1",
49
- "eslint-plugin-unicorn": "^59.0.1",
50
- "globals": "^16.2.0",
51
- "jsdom": "^26.1.0",
52
- "prettier": "^3.5.3",
53
- "prettier-plugin-jsdoc": "^1.3.2",
49
+ "eslint-plugin-svelte": "^3.11.0",
50
+ "eslint-plugin-unicorn": "^60.0.0",
51
+ "globals": "^16.3.0",
52
+ "happy-dom": "^18.0.1",
53
+ "prettier": "^3.6.2",
54
+ "prettier-plugin-jsdoc": "^1.3.3",
54
55
  "prettier-plugin-svelte": "^3.4.0",
55
- "svelte-check": "^4.2.1",
56
+ "svelte-check": "^4.3.1",
56
57
  "type-testing": "^0.2.0",
57
- "typescript": "^5.8.3",
58
- "typescript-eslint": "^8.33.1",
59
- "vite": "^7.0.0",
60
- "vitest": "^3.2.2"
58
+ "typescript": "^5.9.2",
59
+ "typescript-eslint": "^8.39.0",
60
+ "vite": "^7.1.0",
61
+ "vitest": "^3.2.4"
61
62
  },
62
63
  "peerDependencies": {
63
64
  "svelte": "^5"
package/src/Router.svelte CHANGED
@@ -1,20 +1,12 @@
1
1
  <script>
2
2
  import { on } from 'svelte/events';
3
- import { base, componentTree, onGlobalClick, onNavigate } from './create-router.svelte.js';
4
- import { join } from './helpers/utils.js';
3
+ import { componentTree, init, onGlobalClick, onNavigate } from './create-router.svelte.js';
5
4
  import RecursiveComponentTree from './RecursiveComponentTree.svelte';
6
5
 
7
6
  /** @type {{ base?: string }} */
8
7
  let { base: basename } = $props();
9
8
 
10
- if (basename) {
11
- base.name = (basename.startsWith('/') ? '' : '/') + basename;
12
- const url = new URL(globalThis.location.href);
13
- if (!url.pathname.startsWith(base.name)) {
14
- url.pathname = join(base.name, url.pathname);
15
- history.replaceState(history.state || {}, '', url.href);
16
- }
17
- }
9
+ init(basename);
18
10
 
19
11
  onNavigate();
20
12
 
@@ -8,14 +8,20 @@ export function isActiveLink(node, { className = 'is-active', startsWith = false
8
8
  }
9
9
 
10
10
  $effect(() => {
11
- let pathname = new URL(node.href).pathname;
12
- if (base.name) {
13
- pathname = join(base.name, pathname);
11
+ let pathname;
12
+ if (base.name === '#') {
13
+ pathname = new URL(node.href).hash.slice(1);
14
+ } else {
15
+ pathname = new URL(node.href).pathname;
16
+ if (base.name) {
17
+ pathname = join(base.name, pathname);
18
+ }
14
19
  }
20
+ const tokens = className.split(' ').filter(Boolean) ?? [];
15
21
  if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
16
- node.classList.add(className);
22
+ node.classList.add(...tokens);
17
23
  } else {
18
- node.classList.remove(className);
24
+ node.classList.remove(...tokens);
19
25
  }
20
26
  });
21
27
  }
@@ -2,12 +2,23 @@ import { BROWSER, DEV } from 'esm-env';
2
2
  import { isActive } from './helpers/is-active.js';
3
3
  import { matchRoute } from './helpers/match-route.js';
4
4
  import { preload, preloadOnHover } from './helpers/preload.js';
5
- import { constructPath, join, resolveRouteComponents, updatedLocation } from './helpers/utils.js';
5
+ import {
6
+ constructPath,
7
+ join,
8
+ resolveRouteComponents,
9
+ stripBase,
10
+ updatedLocation,
11
+ } from './helpers/utils.js';
6
12
  import { syncSearchParams } from './search-params.svelte.js';
7
13
 
8
14
  /** @type {import('./index.d.ts').Routes} */
9
15
  let routes;
10
16
 
17
+ /** @type {{ name?: string }} */
18
+ export const base = {
19
+ name: undefined,
20
+ };
21
+
11
22
  /** @type {{ value: import('svelte').Component[] }} */
12
23
  export let componentTree = $state({ value: [] });
13
24
 
@@ -21,10 +32,26 @@ let meta = $state({ value: {} });
21
32
  let navigationIndex = 0;
22
33
  let pendingNavigationIndex = 0;
23
34
 
24
- /** @type {{ name?: string }} */
25
- export const base = {
26
- name: undefined,
27
- };
35
+ /** @param {string | undefined} basename */
36
+ export function init(basename) {
37
+ if (basename) {
38
+ const url = new URL(globalThis.location.toString());
39
+ if (basename === '#') {
40
+ base.name = '#';
41
+ if (!globalThis.location.href.includes('#')) {
42
+ url.hash = '/';
43
+ history.replaceState(history.state || {}, '', url.toString());
44
+ }
45
+ } else {
46
+ base.name = (basename.startsWith('/') ? '' : '/') + basename;
47
+ if (!url.pathname.startsWith(base.name)) {
48
+ url.pathname = join(base.name, url.pathname);
49
+ history.replaceState(history.state || {}, '', url.toString());
50
+ }
51
+ }
52
+ }
53
+ Object.assign(location, updatedLocation());
54
+ }
28
55
 
29
56
  /**
30
57
  * @template {import('./index.d.ts').Routes} T
@@ -60,7 +87,7 @@ export function createRouter(r) {
60
87
  return params.value;
61
88
  },
62
89
  get pathname() {
63
- return /** @type {import('./index.d.ts').Path<T>} */ (location.pathname);
90
+ return /** @type {import('./index.d.ts').Path<T>} */ (stripBase(location.pathname));
64
91
  },
65
92
  get search() {
66
93
  return location.search;
@@ -87,18 +114,39 @@ function navigate(path, options = {}) {
87
114
  globalThis.history.go(path);
88
115
  return;
89
116
  }
90
- if (options.params) {
91
- path = constructPath(path, options.params);
92
- }
117
+
118
+ path = constructPath(path, options.params);
93
119
  if (options.search && !options.search.startsWith('?')) {
94
120
  options.search = '?' + options.search;
95
121
  }
96
- if (options.hash && !options.hash.startsWith('#')) {
122
+ if (options.hash && !options.hash.startsWith('#') && base.name !== '#') {
97
123
  options.hash = '#' + options.hash;
98
124
  }
125
+ if (base.name === '#') {
126
+ path = new URL(path).hash;
127
+ }
99
128
  onNavigate(path, options);
100
129
  }
101
130
 
131
+ /** @param {string} [path] */
132
+ function getMatchPath(path) {
133
+ let matchPath = '';
134
+
135
+ if (path) {
136
+ matchPath = path;
137
+ } else if (base.name === '#') {
138
+ matchPath = globalThis.location.hash.slice(1);
139
+ } else {
140
+ matchPath = globalThis.location.pathname;
141
+ }
142
+
143
+ if (base.name && matchPath.startsWith(base.name)) {
144
+ matchPath = matchPath.slice(base.name.length) || '/';
145
+ }
146
+
147
+ return stripBase(matchPath);
148
+ }
149
+
102
150
  /**
103
151
  * @param {string} [path]
104
152
  * @param {import('./index.d.ts').NavigateOptions} options
@@ -111,24 +159,35 @@ export async function onNavigate(path, options = {}) {
111
159
  navigationIndex++;
112
160
  const currentNavigationIndex = navigationIndex;
113
161
 
114
- let matchPath = path || globalThis.location.pathname;
115
- if (base.name && matchPath.startsWith(base.name)) {
116
- matchPath = matchPath.slice(base.name.length) || '/';
117
- }
162
+ let matchPath = getMatchPath(path);
118
163
  const { match, layouts, hooks, meta: newMeta, params: newParams } = matchRoute(matchPath, routes);
119
164
 
120
- for (const { beforeLoad } of hooks) {
165
+ let errorHooks = [];
166
+ for (const hook of hooks) {
121
167
  try {
168
+ const { beforeLoad } = hook;
169
+ errorHooks.push(hook);
122
170
  pendingNavigationIndex = currentNavigationIndex;
123
- await beforeLoad?.({ pathname: matchPath, ...options });
124
- } catch {
171
+ await beforeLoad?.({ pathname: matchPath, meta: newMeta, ...options });
172
+ } catch (error) {
173
+ for (const { onError } of errorHooks) {
174
+ void onError?.(error, { pathname: matchPath, meta: newMeta, ...options });
175
+ }
125
176
  return;
126
177
  }
127
178
  }
128
179
 
129
180
  const fromBeforeLoadHook = new Error().stack?.includes('beforeLoad');
130
181
 
131
- const routeComponents = await resolveRouteComponents(match ? [...layouts, match] : layouts);
182
+ let routeComponents;
183
+ try {
184
+ routeComponents = await resolveRouteComponents(match ? [...layouts, match] : layouts);
185
+ } catch (error) {
186
+ for (const { onError } of hooks) {
187
+ void onError?.(error, { pathname: matchPath, meta: newMeta, ...options });
188
+ }
189
+ throw error;
190
+ }
132
191
  if (
133
192
  navigationIndex !== currentNavigationIndex ||
134
193
  (fromBeforeLoadHook && pendingNavigationIndex + 1 !== currentNavigationIndex)
@@ -137,11 +196,20 @@ export async function onNavigate(path, options = {}) {
137
196
  }
138
197
 
139
198
  if (path) {
140
- if (options.search) path += options.search;
141
- if (options.hash) path += options.hash;
199
+ let url = new URL(globalThis.location.toString());
200
+ url.search = '';
201
+ if (options.search) url.search = options.search;
202
+ if (base.name === '#') {
203
+ url.hash = path;
204
+ } else {
205
+ if (options.hash) path += options.hash;
206
+ url.pathname = base.name ? join(base.name, path) : path;
207
+ }
142
208
  const historyMethod = options.replace ? 'replaceState' : 'pushState';
143
- const to = base.name ? join(base.name, path) : path;
144
- globalThis.history[historyMethod](options.state || {}, '', to);
209
+ globalThis.history[historyMethod](options.state || {}, '', url.toString());
210
+ syncSearchParams(options.search);
211
+ } else {
212
+ syncSearchParams(globalThis.location.search);
145
213
  }
146
214
 
147
215
  if (options.viewTransition && document.startViewTransition !== undefined) {
@@ -153,7 +221,6 @@ export async function onNavigate(path, options = {}) {
153
221
  }
154
222
  params.value = newParams;
155
223
  meta.value = newMeta;
156
- syncSearchParams();
157
224
  Object.assign(location, updatedLocation());
158
225
 
159
226
  if (options.scrollToTop !== false) {
@@ -161,7 +228,7 @@ export async function onNavigate(path, options = {}) {
161
228
  }
162
229
 
163
230
  for (const { afterLoad } of hooks) {
164
- afterLoad?.({ pathname: matchPath, ...options });
231
+ void afterLoad?.({ pathname: matchPath, meta: newMeta, ...options });
165
232
  }
166
233
  }
167
234
 
@@ -176,13 +243,16 @@ export function onGlobalClick(event) {
176
243
  const currentOrigin = globalThis.location.origin;
177
244
  if (url.origin !== currentOrigin) return;
178
245
 
246
+ const path = base.name === '#' ? url.hash : url.pathname;
247
+ const hash = base.name === '#' ? undefined : url.hash;
248
+
179
249
  event.preventDefault();
180
250
  const { replace, state, scrollToTop, viewTransition } = anchor.dataset;
181
- onNavigate(url.pathname, {
251
+ onNavigate(path, {
182
252
  replace: replace === '' || replace === 'true',
183
253
  search: url.search,
184
254
  state,
185
- hash: url.hash,
255
+ hash,
186
256
  scrollToTop: scrollToTop === 'false' ? false : /** @type ScrollBehavior */ (scrollToTop),
187
257
  viewTransition: viewTransition === '' || viewTransition === 'true',
188
258
  });
@@ -177,7 +177,9 @@ export function createRouterCode(routes, routesPath, { allLazy = false } = {}) {
177
177
  `import { createRouter } from 'sv-router';`,
178
178
  ...imports,
179
179
  '',
180
- `export const { p, navigate, isActive, preload, route } = createRouter(${stringifiedRoutes});`,
180
+ `const routes = ${stringifiedRoutes};`,
181
+ 'export type Routes = typeof routes;',
182
+ 'export const { p, navigate, isActive, preload, route } = createRouter(routes);',
181
183
  ].join('\n');
182
184
  }
183
185
 
@@ -7,7 +7,7 @@ import { constructPath, join } from './utils.js';
7
7
  * @returns {boolean}
8
8
  */
9
9
  export function isActive(pathname, params) {
10
- const p = base.name ? join(base.name, pathname) : pathname;
10
+ const p = base.name && base.name !== '#' ? join(base.name, pathname) : pathname;
11
11
  return compare((a, b) => a === b, p, params);
12
12
  }
13
13
 
@@ -17,7 +17,7 @@ export function isActive(pathname, params) {
17
17
  * @returns {boolean}
18
18
  */
19
19
  isActive.startsWith = (pathname, params) => {
20
- const p = base.name ? join(base.name, pathname) : pathname;
20
+ const p = base.name && base.name !== '#' ? join(base.name, pathname) : pathname;
21
21
  return compare((a, b) => a.startsWith(b), p, params);
22
22
  };
23
23
 
@@ -33,11 +33,15 @@ function compare(compareFn, pathname, params) {
33
33
  }
34
34
 
35
35
  if (params) {
36
- return compareFn(location.pathname, constructPath(pathname, params));
36
+ if (base.name === '#') {
37
+ return compareFn(location.pathname, new URL(constructPath(pathname, params)).hash.slice(1));
38
+ } else {
39
+ return compareFn(location.pathname, constructPath(pathname, params));
40
+ }
37
41
  }
38
42
 
39
43
  const pathParts = pathname.split('/').slice(1);
40
- const routeParts = location.pathname.split('/').slice(1);
44
+ let routeParts = location.pathname.split('/').slice(1);
41
45
  if (pathParts.length > routeParts.length) {
42
46
  return false;
43
47
  }
@@ -109,7 +109,7 @@ export function matchRoute(pathname, routes) {
109
109
 
110
110
  const nestedPathname = '/' + pathParts.slice(index + 1).join('/');
111
111
  const result = matchRoute(nestedPathname, routeMatch);
112
- if (result) {
112
+ if (result.match) {
113
113
  match = result.match;
114
114
  params = { ...params, ...result.params };
115
115
  hooks.push(...result.hooks);
@@ -119,6 +119,8 @@ export function matchRoute(pathname, routes) {
119
119
  } else {
120
120
  layouts.push(...result.layouts);
121
121
  }
122
+ } else {
123
+ continue;
122
124
  }
123
125
  break outer;
124
126
  }
@@ -141,7 +143,7 @@ export function sortRoutes(routes) {
141
143
  */
142
144
  function getRoutePriority(route) {
143
145
  if (route === '' || route === '/') return 1;
144
- if (route.startsWith('*')) return 4;
146
+ if (route.includes('*')) return 4;
145
147
  if (route.includes(':')) return 3;
146
148
  return 2;
147
149
  }
@@ -7,9 +7,9 @@ import { resolveRouteComponents } from './utils.js';
7
7
  * @param {import('../index.d.ts').NavigateOptions} [options]
8
8
  */
9
9
  export async function preload(routes, path, options) {
10
- const { match, layouts, hooks } = matchRoute(path, routes);
10
+ const { match, layouts, hooks, meta } = matchRoute(path, routes);
11
11
  for (const { onPreload } of hooks) {
12
- onPreload?.({ pathname: path, ...options });
12
+ void onPreload?.({ pathname: path, meta, ...options });
13
13
  }
14
14
  await resolveRouteComponents(match ? [...layouts, match] : layouts);
15
15
  }
@@ -1,16 +1,26 @@
1
+ import { base } from '../create-router.svelte.js';
2
+
1
3
  /**
2
4
  * @param {string} path
3
5
  * @param {Record<string, string>} [params]
4
6
  * @returns {string}
5
7
  */
6
8
  export function constructPath(path, params) {
7
- if (!params) return path;
9
+ if (params) {
10
+ for (const key in params) {
11
+ path = path.replace(`:${key}`, params[key]);
12
+ }
13
+ }
8
14
 
9
- let result = path;
10
- for (const key in params) {
11
- result = result.replace(`:${key}`, params[key]);
15
+ if (base.name === '#') {
16
+ const url = new URL(globalThis.location.toString());
17
+ url.hash = path;
18
+ url.search = '';
19
+
20
+ return url.toString();
12
21
  }
13
- return result;
22
+
23
+ return path;
14
24
  }
15
25
 
16
26
  /**
@@ -42,7 +52,10 @@ function resolveRouteComponent(input) {
42
52
  * @returns {input is import('../index.d.ts').LazyRouteComponent}
43
53
  */
44
54
  export function isLazyImport(input) {
45
- return typeof input === 'function' && !!/\(\)\s?=>\s?import\(.*\)/.test(String(input));
55
+ return (
56
+ typeof input === 'function' &&
57
+ !!/\(\)\s?=>\s?(import|__vite_ssr_dynamic_import__)\(.*\)/.test(String(input))
58
+ );
46
59
  }
47
60
 
48
61
  /** @param {...string} parts */
@@ -60,11 +73,25 @@ export function join(...parts) {
60
73
  return result;
61
74
  }
62
75
 
76
+ /**
77
+ * @param {string} pathname
78
+ * @returns {string}
79
+ */
80
+ export function stripBase(pathname) {
81
+ if (base.name && pathname.startsWith(base.name)) {
82
+ pathname = pathname.slice(base.name.length) || '/';
83
+ }
84
+ return pathname;
85
+ }
86
+
63
87
  export function updatedLocation() {
88
+ const pathname =
89
+ base.name === '#' ? globalThis.location.hash.slice(1) : globalThis.location.pathname;
90
+ const hash = base.name === '#' ? '' : globalThis.location.hash;
64
91
  return {
65
- pathname: globalThis.location.pathname,
92
+ pathname,
66
93
  search: globalThis.location.search,
67
94
  state: history.state,
68
- hash: globalThis.location.hash,
95
+ hash,
69
96
  };
70
97
  }
package/src/index.d.ts CHANGED
@@ -41,11 +41,12 @@ export const searchParams: SearchParams;
41
41
  type BaseProps = {};
42
42
 
43
43
  export type LazyRouteComponent<Props extends BaseProps = BaseProps> = () => Promise<{
44
- default: Component<Props>;
44
+ default: Component<Props> | Snippet<[Props]>;
45
45
  }>;
46
46
 
47
47
  export type RouteComponent<Props extends BaseProps = any> =
48
48
  | Component<Props>
49
+ | Snippet<[Props]>
49
50
  | LazyRouteComponent<Props>;
50
51
  export type LayoutComponent = RouteComponent<{ children: Snippet }>;
51
52
  export type Hooks = {
@@ -64,9 +65,11 @@ export type Hooks = {
64
65
  */
65
66
  beforeLoad?(context: HooksContext): void | Promise<void>;
66
67
  /** A function that will be called after the route is loaded. */
67
- afterLoad?(context: HooksContext): void;
68
+ afterLoad?(context: HooksContext): void | Promise<void>;
68
69
  /** A function that will be called when the route is preloaded. */
69
- onPreload?(context: HooksContext): void;
70
+ onPreload?(context: HooksContext): void | Promise<void>;
71
+ /** A function that will be called when the route fails to load. */
72
+ onError?(error: unknown, context: HooksContext): void | Promise<void>;
70
73
  };
71
74
 
72
75
  export type Routes = {
@@ -79,7 +82,11 @@ export type Routes = {
79
82
 
80
83
  export type IsActiveLink = Action<
81
84
  HTMLAnchorElement,
82
- { className?: string; startsWith?: boolean } | undefined
85
+ | {
86
+ className?: string;
87
+ startsWith?: boolean;
88
+ }
89
+ | undefined
83
90
  >;
84
91
 
85
92
  /**
@@ -143,7 +150,7 @@ export type RouterApi<T extends Routes> = {
143
150
  */
144
151
  isActive: {
145
152
  <U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
146
- startsWith<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
153
+ startsWith<U extends Path<T>>(...args: IsActiveArgs<U, true>): boolean;
147
154
  };
148
155
 
149
156
  /**
@@ -190,27 +197,39 @@ export type Path<T extends Routes, AnyParam extends boolean = false> = RemovePar
190
197
  RemoveLastSlash<RecursiveKeys<StripNonRoutes<T>, '', AnyParam>>
191
198
  >;
192
199
 
193
- export type ConstructPathArgs<T extends string> =
194
- PathParams<T> extends never ? [T] : [T, PathParams<T>];
200
+ export type ConstructPathArgs<TPath extends string> = {
201
+ [Path in TPath]: PathParams<Path> extends never ? [Path] : [Path, PathParams<Path>];
202
+ }[TPath];
195
203
 
196
- export type IsActiveArgs<T extends string> =
197
- PathParams<T> extends never ? [T] : [T] | [T, PathParams<T>];
204
+ export type IsActiveArgs<
205
+ TPath extends string,
206
+ StartsWith extends boolean = false,
207
+ > = StartsWith extends true
208
+ ? {
209
+ [Path in TPath]: PathParams<Path> extends never
210
+ ? [PathPrefixes<Path>]
211
+ : [PathPrefixes<Path>] | [PathPrefixes<Path>, PathParams<Path>];
212
+ }[TPath]
213
+ : {
214
+ [Path in TPath]: PathParams<Path> extends never ? [Path] : [Path] | [Path, PathParams<Path>];
215
+ }[TPath];
198
216
 
199
- export type PathParams<T extends string> =
200
- ExtractParams<RemoveParenthesis<T>> extends never
217
+ export type PathParams<TPath extends string> =
218
+ ExtractParams<RemoveParenthesis<TPath>> extends never
201
219
  ? never
202
- : Record<ExtractParams<RemoveParenthesis<T>>, string>;
220
+ : Record<ExtractParams<RemoveParenthesis<TPath>>, string>;
203
221
 
204
- export type AllParams<T extends Routes> = Partial<
205
- Record<ExtractParams<RemoveParenthesis<RecursiveKeys<T>>>, string>
222
+ export type AllParams<TRoutes extends Routes> = Partial<
223
+ Record<ExtractParams<RemoveParenthesis<RecursiveKeys<TRoutes>>>, string>
206
224
  >;
207
225
 
208
226
  export type HooksContext = {
227
+ hash?: string;
228
+ meta: RouteMeta;
209
229
  pathname: string;
210
230
  replace?: boolean;
211
231
  search?: string;
212
232
  state?: string;
213
- hash?: string;
214
233
  };
215
234
 
216
235
  export type NavigateOptions =
@@ -224,7 +243,7 @@ export type NavigateOptions =
224
243
  }
225
244
  | undefined;
226
245
 
227
- export type SearchParams = URLSearchParams & {
246
+ export type SearchParams = Omit<URLSearchParams, 'append' | 'delete' | 'set' | 'sort'> & {
228
247
  append: (name: string, value: string, options?: { replace?: boolean }) => void;
229
248
  delete: (name: string, value?: string, options?: { replace?: boolean }) => void;
230
249
  set: (name: string, value: string, options?: { replace?: boolean }) => void;
@@ -288,3 +307,15 @@ type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${inf
288
307
  ? never
289
308
  : Param
290
309
  : never;
310
+
311
+ type PathPrefixes<T extends string, Acc extends string = ''> = T extends '/'
312
+ ? '/'
313
+ : T extends `/${infer Segment}/${infer Rest}`
314
+ ? Acc extends ''
315
+ ? PathPrefixes<`/${Rest}`, `/${Segment}`> | `/${Segment}`
316
+ : PathPrefixes<`/${Rest}`, `${Acc}/${Segment}`> | `${Acc}/${Segment}` | Acc
317
+ : T extends `/${infer Segment}`
318
+ ? Acc extends ''
319
+ ? `/${Segment}`
320
+ : `${Acc}/${Segment}` | Acc
321
+ : Acc;
@@ -55,22 +55,21 @@ const shell = {
55
55
 
56
56
  export { shell as searchParams };
57
57
 
58
- export function syncSearchParams() {
59
- const newSearchParams = new URLSearchParams(globalThis.location.search);
60
- if (searchParams.toString() === newSearchParams.toString()) {
61
- return;
62
- }
63
- searchParams = new SvelteURLSearchParams();
64
- for (const [key, value] of newSearchParams.entries()) {
65
- searchParams.append(key, value);
58
+ /** @param {string} [search] */
59
+ export function syncSearchParams(search) {
60
+ if (searchParams.toString() !== search) {
61
+ searchParams = new SvelteURLSearchParams();
62
+ const newSearchParams = new URLSearchParams(search);
63
+ for (const [key, value] of newSearchParams.entries()) {
64
+ searchParams.append(key, value);
65
+ }
66
66
  }
67
67
  }
68
68
 
69
69
  /** @param {{ replace?: boolean }} [options] */
70
70
  function updateUrlSearchParams(options) {
71
- let url = globalThis.location.origin + globalThis.location.pathname;
72
- if (searchParams.size > 0) {
73
- url += '?' + searchParams.toString();
74
- }
71
+ let url = new URL(globalThis.location.toString());
72
+ url.search = searchParams.toString();
73
+
75
74
  globalThis.history[options?.replace ? 'replaceState' : 'pushState']({}, '', url);
76
75
  }