sv-router 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sv-router",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Modern Svelte Routing",
5
5
  "keywords": [
6
6
  "svelte",
@@ -38,7 +38,7 @@
38
38
  "devDependencies": {
39
39
  "@changesets/cli": "^2.29.4",
40
40
  "@eslint/js": "^9.28.0",
41
- "@sveltejs/vite-plugin-svelte": "^5.1.0",
41
+ "@sveltejs/vite-plugin-svelte": "^6.0.0",
42
42
  "@testing-library/jest-dom": "^6.6.3",
43
43
  "@testing-library/svelte": "^5.2.8",
44
44
  "@types/node": "^22.15.30",
@@ -12,10 +12,11 @@ export function isActiveLink(node, { className = 'is-active', startsWith = false
12
12
  if (base.name) {
13
13
  pathname = join(base.name, pathname);
14
14
  }
15
+ const tokens = className.split(' ').filter(Boolean) ?? [];
15
16
  if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
16
- node.classList.add(className);
17
+ node.classList.add(...tokens);
17
18
  } else {
18
- node.classList.remove(className);
19
+ node.classList.remove(...tokens);
19
20
  }
20
21
  });
21
22
  }
@@ -2,7 +2,13 @@ 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} */
@@ -60,7 +66,7 @@ export function createRouter(r) {
60
66
  return params.value;
61
67
  },
62
68
  get pathname() {
63
- return /** @type {import('./index.d.ts').Path<T>} */ (location.pathname);
69
+ return /** @type {import('./index.d.ts').Path<T>} */ (stripBase(location.pathname));
64
70
  },
65
71
  get search() {
66
72
  return location.search;
@@ -111,16 +117,13 @@ export async function onNavigate(path, options = {}) {
111
117
  navigationIndex++;
112
118
  const currentNavigationIndex = navigationIndex;
113
119
 
114
- let matchPath = path || globalThis.location.pathname;
115
- if (base.name && matchPath.startsWith(base.name)) {
116
- matchPath = matchPath.slice(base.name.length) || '/';
117
- }
120
+ const matchPath = stripBase(path || globalThis.location.pathname);
118
121
  const { match, layouts, hooks, meta: newMeta, params: newParams } = matchRoute(matchPath, routes);
119
122
 
120
123
  for (const { beforeLoad } of hooks) {
121
124
  try {
122
125
  pendingNavigationIndex = currentNavigationIndex;
123
- await beforeLoad?.({ pathname: matchPath, ...options });
126
+ await beforeLoad?.({ pathname: matchPath, meta: newMeta, ...options });
124
127
  } catch {
125
128
  return;
126
129
  }
@@ -161,7 +164,7 @@ export async function onNavigate(path, options = {}) {
161
164
  }
162
165
 
163
166
  for (const { afterLoad } of hooks) {
164
- afterLoad?.({ pathname: matchPath, ...options });
167
+ void afterLoad?.({ pathname: matchPath, meta: newMeta, ...options });
165
168
  }
166
169
  }
167
170
 
@@ -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
 
@@ -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,3 +1,5 @@
1
+ import { base } from '../create-router.svelte.js';
2
+
1
3
  /**
2
4
  * @param {string} path
3
5
  * @param {Record<string, string>} [params]
@@ -60,6 +62,17 @@ export function join(...parts) {
60
62
  return result;
61
63
  }
62
64
 
65
+ /**
66
+ * @param {string} pathname
67
+ * @returns {string}
68
+ */
69
+ export function stripBase(pathname) {
70
+ if (base.name && pathname.startsWith(base.name)) {
71
+ pathname = pathname.slice(base.name.length) || '/';
72
+ }
73
+ return pathname;
74
+ }
75
+
63
76
  export function updatedLocation() {
64
77
  return {
65
78
  pathname: globalThis.location.pathname,
package/src/index.d.ts CHANGED
@@ -64,9 +64,9 @@ export type Hooks = {
64
64
  */
65
65
  beforeLoad?(context: HooksContext): void | Promise<void>;
66
66
  /** A function that will be called after the route is loaded. */
67
- afterLoad?(context: HooksContext): void;
67
+ afterLoad?(context: HooksContext): void | Promise<void>;
68
68
  /** A function that will be called when the route is preloaded. */
69
- onPreload?(context: HooksContext): void;
69
+ onPreload?(context: HooksContext): void | Promise<void>;
70
70
  };
71
71
 
72
72
  export type Routes = {
@@ -79,7 +79,11 @@ export type Routes = {
79
79
 
80
80
  export type IsActiveLink = Action<
81
81
  HTMLAnchorElement,
82
- { className?: string; startsWith?: boolean } | undefined
82
+ | {
83
+ className?: string;
84
+ startsWith?: boolean;
85
+ }
86
+ | undefined
83
87
  >;
84
88
 
85
89
  /**
@@ -143,7 +147,7 @@ export type RouterApi<T extends Routes> = {
143
147
  */
144
148
  isActive: {
145
149
  <U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
146
- startsWith<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
150
+ startsWith<U extends Path<T>>(...args: IsActiveArgs<U, true>): boolean;
147
151
  };
148
152
 
149
153
  /**
@@ -190,27 +194,39 @@ export type Path<T extends Routes, AnyParam extends boolean = false> = RemovePar
190
194
  RemoveLastSlash<RecursiveKeys<StripNonRoutes<T>, '', AnyParam>>
191
195
  >;
192
196
 
193
- export type ConstructPathArgs<T extends string> =
194
- PathParams<T> extends never ? [T] : [T, PathParams<T>];
197
+ export type ConstructPathArgs<TPath extends string> = {
198
+ [Path in TPath]: PathParams<Path> extends never ? [Path] : [Path, PathParams<Path>];
199
+ }[TPath];
195
200
 
196
- export type IsActiveArgs<T extends string> =
197
- PathParams<T> extends never ? [T] : [T] | [T, PathParams<T>];
201
+ export type IsActiveArgs<
202
+ TPath extends string,
203
+ StartsWith extends boolean = false,
204
+ > = StartsWith extends true
205
+ ? {
206
+ [Path in TPath]: PathParams<Path> extends never
207
+ ? [PathPrefixes<Path>]
208
+ : [PathPrefixes<Path>] | [PathPrefixes<Path>, PathParams<Path>];
209
+ }[TPath]
210
+ : {
211
+ [Path in TPath]: PathParams<Path> extends never ? [Path] : [Path] | [Path, PathParams<Path>];
212
+ }[TPath];
198
213
 
199
- export type PathParams<T extends string> =
200
- ExtractParams<RemoveParenthesis<T>> extends never
214
+ export type PathParams<TPath extends string> =
215
+ ExtractParams<RemoveParenthesis<TPath>> extends never
201
216
  ? never
202
- : Record<ExtractParams<RemoveParenthesis<T>>, string>;
217
+ : Record<ExtractParams<RemoveParenthesis<TPath>>, string>;
203
218
 
204
- export type AllParams<T extends Routes> = Partial<
205
- Record<ExtractParams<RemoveParenthesis<RecursiveKeys<T>>>, string>
219
+ export type AllParams<TRoutes extends Routes> = Partial<
220
+ Record<ExtractParams<RemoveParenthesis<RecursiveKeys<TRoutes>>>, string>
206
221
  >;
207
222
 
208
223
  export type HooksContext = {
224
+ hash?: string;
225
+ meta: RouteMeta;
209
226
  pathname: string;
210
227
  replace?: boolean;
211
228
  search?: string;
212
229
  state?: string;
213
- hash?: string;
214
230
  };
215
231
 
216
232
  export type NavigateOptions =
@@ -288,3 +304,15 @@ type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${inf
288
304
  ? never
289
305
  : Param
290
306
  : never;
307
+
308
+ type PathPrefixes<T extends string, Acc extends string = ''> = T extends '/'
309
+ ? '/'
310
+ : T extends `/${infer Segment}/${infer Rest}`
311
+ ? Acc extends ''
312
+ ? PathPrefixes<`/${Rest}`, `/${Segment}`> | `/${Segment}`
313
+ : PathPrefixes<`/${Rest}`, `${Acc}/${Segment}`> | `${Acc}/${Segment}` | Acc
314
+ : T extends `/${infer Segment}`
315
+ ? Acc extends ''
316
+ ? `/${Segment}`
317
+ : `${Acc}/${Segment}` | Acc
318
+ : Acc;