sv-router 0.18.1 → 0.19.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/README.md +1 -1
- package/package.json +20 -20
- package/src/attachments.svelte.js +8 -8
- package/src/create-router.svelte.js +25 -11
- package/src/helpers/is-active.js +12 -33
- package/src/helpers/preload.js +2 -1
- package/src/helpers/utils.js +36 -0
- package/src/index.d.ts +21 -8
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
Flexible, ergonomic, and complete Svelte 5 router.
|
|
13
13
|
|
|
14
|
-
[Documentation](https://sv-router.
|
|
14
|
+
[Documentation](https://sv-router.dev/) • [Getting Started](https://sv-router.dev/guide/getting-started) • [Reference](https://sv-router.dev/reference)
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Type-safe routing for Svelte SPAs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"router",
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
"type": "git",
|
|
17
17
|
"url": "git+https://github.com/colinlienard/sv-router.git"
|
|
18
18
|
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"sv-router": "./src/cli/index.js"
|
|
21
|
+
},
|
|
19
22
|
"files": [
|
|
20
23
|
"src"
|
|
21
24
|
],
|
|
@@ -39,28 +42,28 @@
|
|
|
39
42
|
"esm-env": "^1.2.2"
|
|
40
43
|
},
|
|
41
44
|
"devDependencies": {
|
|
42
|
-
"@changesets/cli": "^
|
|
45
|
+
"@changesets/cli": "^3.0.2",
|
|
43
46
|
"@eslint/js": "^10.0.1",
|
|
44
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
45
|
-
"@testing-library/jest-dom": "^7.0.
|
|
47
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
48
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
46
49
|
"@testing-library/svelte": "^5.4.2",
|
|
47
|
-
"@testing-library/user-event": "^14.6.
|
|
48
|
-
"@types/node": "^24.13.
|
|
49
|
-
"@vitest/coverage-v8": "^
|
|
50
|
-
"eslint": "^10.
|
|
50
|
+
"@testing-library/user-event": "^14.6.7",
|
|
51
|
+
"@types/node": "^24.13.4",
|
|
52
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
53
|
+
"eslint": "^10.10.0",
|
|
51
54
|
"eslint-config-prettier": "^10.1.8",
|
|
52
55
|
"eslint-plugin-simple-import-sort": "^14.0.0",
|
|
53
|
-
"eslint-plugin-svelte": "^3.
|
|
54
|
-
"eslint-plugin-unicorn": "^
|
|
55
|
-
"globals": "^17.
|
|
56
|
-
"happy-dom": "^20.
|
|
57
|
-
"oxfmt": "^0.
|
|
58
|
-
"svelte-check": "^4.7.
|
|
56
|
+
"eslint-plugin-svelte": "^3.23.0",
|
|
57
|
+
"eslint-plugin-unicorn": "^74.0.0",
|
|
58
|
+
"globals": "^17.12.0",
|
|
59
|
+
"happy-dom": "^20.14.3",
|
|
60
|
+
"oxfmt": "^0.67.0",
|
|
61
|
+
"svelte-check": "^4.7.6",
|
|
59
62
|
"type-testing": "^0.2.0",
|
|
60
63
|
"typescript": "^6.0.3",
|
|
61
|
-
"typescript-eslint": "^8.
|
|
62
|
-
"vite": "^8.
|
|
63
|
-
"vitest": "^
|
|
64
|
+
"typescript-eslint": "^8.70.0",
|
|
65
|
+
"vite": "^8.2.2",
|
|
66
|
+
"vitest": "^5.0.0"
|
|
64
67
|
},
|
|
65
68
|
"peerDependencies": {
|
|
66
69
|
"svelte": "^5"
|
|
@@ -79,8 +82,5 @@
|
|
|
79
82
|
"format": "oxfmt --check",
|
|
80
83
|
"format:fix": "oxfmt",
|
|
81
84
|
"changeset": "changeset"
|
|
82
|
-
},
|
|
83
|
-
"bin": {
|
|
84
|
-
"sv-router": "./src/cli/index.js"
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { base, location } from './create-router.svelte.js';
|
|
2
|
+
import { comparePathParts, toPathParts } from './helpers/utils.js';
|
|
2
3
|
|
|
3
4
|
/** @type {import('./index.d.ts').IsActiveLink} */
|
|
4
5
|
export function isActiveLink({ className = 'is-active', startsWith = false } = {}) {
|
|
@@ -8,14 +9,13 @@ export function isActiveLink({ className = 'is-active', startsWith = false } = {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
$effect(() => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
pathname
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
|
|
12
|
+
const url = new URL(node.href);
|
|
13
|
+
const pathname = base.name === '#' ? url.hash.slice(1) : url.pathname;
|
|
14
|
+
const tokens = className.split(' ').filter(Boolean);
|
|
15
|
+
const isActive =
|
|
16
|
+
pathname !== '' &&
|
|
17
|
+
comparePathParts(toPathParts(pathname), toPathParts(location.pathname), startsWith);
|
|
18
|
+
if (isActive) {
|
|
19
19
|
node.classList.add(...tokens);
|
|
20
20
|
} else {
|
|
21
21
|
node.classList.remove(...tokens);
|
|
@@ -147,7 +147,7 @@ export function createRouter(r, options = {}) {
|
|
|
147
147
|
return params.value;
|
|
148
148
|
},
|
|
149
149
|
getParams(pathname) {
|
|
150
|
-
if (!isActive(pathname)) {
|
|
150
|
+
if (!isActive.startsWith(pathname)) {
|
|
151
151
|
throw new Error(`\`${pathname}\` does not match the current route`);
|
|
152
152
|
}
|
|
153
153
|
return params.value;
|
|
@@ -220,21 +220,21 @@ export async function onNavigate(path, options = {}) {
|
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
const popstateDelta = path ? 0 : historyIndex - (history.state?._routerIndex ?? 0);
|
|
224
|
+
function revertPopstate() {
|
|
225
|
+
if (path || popstateDelta === 0) return;
|
|
226
|
+
isSkipNextPopstate = true;
|
|
227
|
+
history.go(popstateDelta);
|
|
228
|
+
}
|
|
229
|
+
|
|
223
230
|
if (navigationBlockers.size > 0) {
|
|
224
|
-
const popstateDelta = path ? 0 : historyIndex - (history.state?._routerIndex ?? 0);
|
|
225
231
|
for (const blocker of navigationBlockers.values()) {
|
|
226
232
|
const shouldNavigate = typeof blocker === 'object' ? blocker.onNavigate : blocker;
|
|
227
233
|
if (!(await shouldNavigate())) {
|
|
228
|
-
|
|
229
|
-
isSkipNextPopstate = true;
|
|
230
|
-
history.go(popstateDelta);
|
|
231
|
-
}
|
|
234
|
+
revertPopstate();
|
|
232
235
|
return;
|
|
233
236
|
}
|
|
234
237
|
}
|
|
235
|
-
if (!path) {
|
|
236
|
-
historyIndex = history.state?._routerIndex ?? historyIndex;
|
|
237
|
-
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
if (pendingController && pendingController !== currentNavigationController) {
|
|
@@ -250,7 +250,13 @@ export async function onNavigate(path, options = {}) {
|
|
|
250
250
|
|
|
251
251
|
const navContext = path ? options : { ...options, ...updatedLocation() };
|
|
252
252
|
const search = parseSearch(navContext.search);
|
|
253
|
-
const hooksContext = {
|
|
253
|
+
const hooksContext = {
|
|
254
|
+
...navContext,
|
|
255
|
+
pathname: matchPath,
|
|
256
|
+
meta: newMeta,
|
|
257
|
+
params: newParams,
|
|
258
|
+
search,
|
|
259
|
+
};
|
|
254
260
|
|
|
255
261
|
let errorHooks = [];
|
|
256
262
|
for (const hook of hooks) {
|
|
@@ -259,7 +265,11 @@ export async function onNavigate(path, options = {}) {
|
|
|
259
265
|
const { beforeLoad } = hook;
|
|
260
266
|
errorHooks.push(hook);
|
|
261
267
|
pendingController = currentNavigationController;
|
|
262
|
-
await beforeLoad?.(hooksContext)
|
|
268
|
+
if ((await beforeLoad?.(hooksContext)) === false) {
|
|
269
|
+
if (signal.aborted) return currentNavigationPromise;
|
|
270
|
+
revertPopstate();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
263
273
|
} catch (error) {
|
|
264
274
|
if (signal.aborted) return currentNavigationPromise;
|
|
265
275
|
for (const { onError } of errorHooks) {
|
|
@@ -271,6 +281,10 @@ export async function onNavigate(path, options = {}) {
|
|
|
271
281
|
}
|
|
272
282
|
}
|
|
273
283
|
|
|
284
|
+
if (!path) {
|
|
285
|
+
historyIndex = history.state?._routerIndex ?? historyIndex;
|
|
286
|
+
}
|
|
287
|
+
|
|
274
288
|
let routeComponents;
|
|
275
289
|
try {
|
|
276
290
|
routeComponents = await resolveRouteComponents(match ? [...layouts, match] : layouts);
|
package/src/helpers/is-active.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { location } from '../create-router.svelte.js';
|
|
2
|
+
import { comparePathParts, stripBase, toPathParts } from './utils.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @param {string} pathname
|
|
@@ -7,7 +7,7 @@ import { constructPath, stripBase } from './utils.js';
|
|
|
7
7
|
* @returns {boolean}
|
|
8
8
|
*/
|
|
9
9
|
export function isActive(pathname, params) {
|
|
10
|
-
return compare(
|
|
10
|
+
return compare(pathname, false, params);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -16,41 +16,20 @@ export function isActive(pathname, params) {
|
|
|
16
16
|
* @returns {boolean}
|
|
17
17
|
*/
|
|
18
18
|
isActive.startsWith = (pathname, params) => {
|
|
19
|
-
return compare(
|
|
19
|
+
return compare(pathname, true, params);
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* @param {function(string, string): boolean} compareFn
|
|
24
23
|
* @param {string} pathname
|
|
24
|
+
* @param {boolean} startsWith
|
|
25
25
|
* @param {Record<string, string>} [params]
|
|
26
26
|
* @returns {boolean}
|
|
27
27
|
*/
|
|
28
|
-
function compare(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (base.name === '#') {
|
|
36
|
-
return compareFn(location.pathname, constructPath(pathname, params).replace('/#', ''));
|
|
37
|
-
}
|
|
38
|
-
return compareFn(location.pathname, constructPath(pathname, params));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const pathParts = pathname.split('/').slice(1);
|
|
42
|
-
let routeParts = stripBase(location.pathname).split('/').slice(1);
|
|
43
|
-
if (pathParts.length > routeParts.length) {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
for (const [index, pathPart] of pathParts.entries()) {
|
|
47
|
-
if (pathPart.startsWith(':')) {
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
const routePart = routeParts[index];
|
|
51
|
-
if (pathPart !== routePart) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return true;
|
|
28
|
+
function compare(pathname, startsWith, params) {
|
|
29
|
+
return comparePathParts(
|
|
30
|
+
toPathParts(pathname),
|
|
31
|
+
toPathParts(stripBase(location.pathname)),
|
|
32
|
+
startsWith,
|
|
33
|
+
params,
|
|
34
|
+
);
|
|
56
35
|
}
|
package/src/helpers/preload.js
CHANGED
|
@@ -12,11 +12,12 @@ const PREDICT_TIMEOUT = 50;
|
|
|
12
12
|
* @param {import('../index.d.ts').NavigateOptions} [options]
|
|
13
13
|
*/
|
|
14
14
|
export async function preload(routes, path, options) {
|
|
15
|
-
const { match, layouts, hooks, meta } = matchRoute(path, routes);
|
|
15
|
+
const { match, layouts, hooks, meta, params } = matchRoute(path, routes);
|
|
16
16
|
for (const { onPreload } of hooks) {
|
|
17
17
|
void onPreload?.({
|
|
18
18
|
pathname: path,
|
|
19
19
|
meta,
|
|
20
|
+
params,
|
|
20
21
|
...options,
|
|
21
22
|
search: parseSearch(options?.search),
|
|
22
23
|
});
|
package/src/helpers/utils.js
CHANGED
|
@@ -92,6 +92,42 @@ export function join(...parts) {
|
|
|
92
92
|
return result;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} pathname
|
|
97
|
+
* @returns {string[]}
|
|
98
|
+
*/
|
|
99
|
+
export function toPathParts(pathname) {
|
|
100
|
+
return pathname.replace(/\/+$/, '').split('/').slice(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {string[]} templateParts
|
|
105
|
+
* @param {string[]} pathParts
|
|
106
|
+
* @param {boolean} [startsWith]
|
|
107
|
+
* @param {Record<string, string>} [params]
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
export function comparePathParts(templateParts, pathParts, startsWith, params) {
|
|
111
|
+
if (
|
|
112
|
+
startsWith ? pathParts.length < templateParts.length : pathParts.length !== templateParts.length
|
|
113
|
+
) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const [index, templatePart] of templateParts.entries()) {
|
|
118
|
+
const pathPart = pathParts[index];
|
|
119
|
+
if (templatePart.startsWith(':')) {
|
|
120
|
+
const param = params?.[templatePart.slice(1)];
|
|
121
|
+
if (param !== undefined && encodeURIComponent(param) !== pathPart) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
} else if (templatePart.toLowerCase() !== pathPart.toLowerCase()) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
95
131
|
/**
|
|
96
132
|
* @param {string} pathname
|
|
97
133
|
* @returns {string}
|
package/src/index.d.ts
CHANGED
|
@@ -119,8 +119,18 @@ export type Hooks = {
|
|
|
119
119
|
* throw navigate('/home');
|
|
120
120
|
* }
|
|
121
121
|
* ```
|
|
122
|
+
*
|
|
123
|
+
* Returning `false` cancels the navigation and stays on the current route.
|
|
124
|
+
*
|
|
125
|
+
* ```js
|
|
126
|
+
* beforeLoad({ meta }) {
|
|
127
|
+
* if (meta.requiresAuth && !user) {
|
|
128
|
+
* return false;
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
122
132
|
*/
|
|
123
|
-
beforeLoad?(context: HooksContext): void | Promise<void>;
|
|
133
|
+
beforeLoad?(context: HooksContext): void | boolean | Promise<void | boolean>;
|
|
124
134
|
/** A function that will be called after the route is loaded. */
|
|
125
135
|
afterLoad?(context: HooksContext): void | Promise<void>;
|
|
126
136
|
/** A function that will be called when the route is preloaded. */
|
|
@@ -304,6 +314,7 @@ export type Search = string | Record<string, string | number | boolean>;
|
|
|
304
314
|
export type HooksContext = {
|
|
305
315
|
hash?: string;
|
|
306
316
|
meta: RouteMeta;
|
|
317
|
+
params: Record<string, string>;
|
|
307
318
|
pathname: string;
|
|
308
319
|
replace?: boolean;
|
|
309
320
|
search: Record<string, string | number | boolean>;
|
|
@@ -350,17 +361,19 @@ type NavigateArgs<T extends string> =
|
|
|
350
361
|
| [number];
|
|
351
362
|
|
|
352
363
|
type StripNonRoutes<T extends Routes> = {
|
|
353
|
-
[
|
|
354
|
-
|
|
355
|
-
: K extends `(*${string})`
|
|
364
|
+
[
|
|
365
|
+
K in keyof T as K extends `*${string}`
|
|
356
366
|
? never
|
|
357
|
-
: K extends
|
|
367
|
+
: K extends `(*${string})`
|
|
358
368
|
? never
|
|
359
|
-
: K extends '
|
|
369
|
+
: K extends 'layout'
|
|
360
370
|
? never
|
|
361
|
-
: K extends '
|
|
371
|
+
: K extends 'hooks'
|
|
362
372
|
? never
|
|
363
|
-
: K
|
|
373
|
+
: K extends 'meta'
|
|
374
|
+
? never
|
|
375
|
+
: K
|
|
376
|
+
]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
|
|
364
377
|
};
|
|
365
378
|
|
|
366
379
|
type NormalizeSlashes<T extends string> = T extends `${infer A}//${infer B}`
|