sv-router 0.13.0 → 0.14.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 +16 -16
- package/src/attachments.svelte.js +47 -0
- package/src/create-router.svelte.js +23 -15
- package/src/helpers/is-active.js +2 -1
- package/src/helpers/match-route.js +1 -1
- package/src/index.d.ts +25 -6
- package/src/index.js +1 -1
- package/src/actions.svelte.js +0 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Modern Svelte Routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -37,28 +37,28 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@changesets/cli": "^2.29.8",
|
|
40
|
-
"@eslint/js": "^
|
|
41
|
-
"@sveltejs/vite-plugin-svelte": "^6.2.
|
|
40
|
+
"@eslint/js": "^10.0.1",
|
|
41
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
42
42
|
"@testing-library/jest-dom": "^6.9.1",
|
|
43
|
-
"@testing-library/svelte": "^5.
|
|
43
|
+
"@testing-library/svelte": "^5.3.1",
|
|
44
44
|
"@testing-library/user-event": "^14.6.1",
|
|
45
|
-
"@types/node": "^24.10.
|
|
46
|
-
"eslint": "^
|
|
45
|
+
"@types/node": "^24.10.13",
|
|
46
|
+
"eslint": "^10.0.0",
|
|
47
47
|
"eslint-config-prettier": "^10.1.8",
|
|
48
48
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
49
|
-
"eslint-plugin-svelte": "^3.
|
|
50
|
-
"eslint-plugin-unicorn": "^
|
|
51
|
-
"globals": "^17.
|
|
52
|
-
"happy-dom": "^20.0
|
|
53
|
-
"prettier": "^3.
|
|
49
|
+
"eslint-plugin-svelte": "^3.15.0",
|
|
50
|
+
"eslint-plugin-unicorn": "^63.0.0",
|
|
51
|
+
"globals": "^17.3.0",
|
|
52
|
+
"happy-dom": "^20.6.0",
|
|
53
|
+
"prettier": "^3.8.1",
|
|
54
54
|
"prettier-plugin-jsdoc": "^1.8.0",
|
|
55
|
-
"prettier-plugin-svelte": "^3.4.
|
|
56
|
-
"svelte-check": "^4.3.
|
|
55
|
+
"prettier-plugin-svelte": "^3.4.1",
|
|
56
|
+
"svelte-check": "^4.3.6",
|
|
57
57
|
"type-testing": "^0.2.0",
|
|
58
58
|
"typescript": "^5.9.3",
|
|
59
|
-
"typescript-eslint": "^8.
|
|
60
|
-
"vite": "^7.
|
|
61
|
-
"vitest": "^4.0.
|
|
59
|
+
"typescript-eslint": "^8.55.0",
|
|
60
|
+
"vite": "^7.3.1",
|
|
61
|
+
"vitest": "^4.0.18"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"svelte": "^5"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { base, location } from './create-router.svelte.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('./index.d.ts').IsActiveLink} */
|
|
4
|
+
export function isActiveLink({ className = 'is-active', startsWith = false } = {}) {
|
|
5
|
+
return (node) => {
|
|
6
|
+
if (node.tagName !== 'A') {
|
|
7
|
+
throw new Error('isActiveLink can only be used on <a> elements');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
$effect(() => {
|
|
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
|
+
}
|
|
17
|
+
const tokens = className.split(' ').filter(Boolean) ?? [];
|
|
18
|
+
if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
|
|
19
|
+
node.classList.add(...tokens);
|
|
20
|
+
} else {
|
|
21
|
+
node.classList.remove(...tokens);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @type {import('./index.d.ts').IsActiveLinkAction} */
|
|
28
|
+
export function isActiveLinkAction(node, { className = 'is-active', startsWith = false } = {}) {
|
|
29
|
+
if (node.tagName !== 'A') {
|
|
30
|
+
throw new Error('isActiveLink can only be used on <a> elements');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
$effect(() => {
|
|
34
|
+
let pathname;
|
|
35
|
+
if (base.name === '#') {
|
|
36
|
+
pathname = new URL(node.href).hash.slice(1);
|
|
37
|
+
} else {
|
|
38
|
+
pathname = new URL(node.href).pathname;
|
|
39
|
+
}
|
|
40
|
+
const tokens = className.split(' ').filter(Boolean) ?? [];
|
|
41
|
+
if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
|
|
42
|
+
node.classList.add(...tokens);
|
|
43
|
+
} else {
|
|
44
|
+
node.classList.remove(...tokens);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -46,8 +46,12 @@ let meta = $state({ value: {} });
|
|
|
46
46
|
/** @type {(() => boolean) | null} */
|
|
47
47
|
let navigationBlocker = null;
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
let
|
|
49
|
+
/** @type {AbortController | null} */
|
|
50
|
+
let currentNavigationController = null;
|
|
51
|
+
let pendingController = /** @type {AbortController | null} */ (null);
|
|
52
|
+
|
|
53
|
+
/** @type {Promise<void | null> | null} */
|
|
54
|
+
let currentNavigationPromise = null;
|
|
51
55
|
|
|
52
56
|
/** @param {string | undefined} basename */
|
|
53
57
|
export function init(basename) {
|
|
@@ -129,7 +133,7 @@ export function createRouter(r) {
|
|
|
129
133
|
* search?: import('./index.d.ts').Search;
|
|
130
134
|
* }} options
|
|
131
135
|
*/
|
|
132
|
-
function navigate(path, options = {}) {
|
|
136
|
+
async function navigate(path, options = {}) {
|
|
133
137
|
if (typeof path === 'number') {
|
|
134
138
|
globalThis.history.go(path);
|
|
135
139
|
return new Navigation(`History entry: ${path}`);
|
|
@@ -141,7 +145,9 @@ function navigate(path, options = {}) {
|
|
|
141
145
|
} else if (options.hash && !options.hash.startsWith('#')) {
|
|
142
146
|
options.hash = '#' + options.hash;
|
|
143
147
|
}
|
|
144
|
-
onNavigate(path, options);
|
|
148
|
+
const promise = onNavigate(path, options);
|
|
149
|
+
currentNavigationPromise = promise;
|
|
150
|
+
await promise;
|
|
145
151
|
return new Navigation(`${path}${serializeSearch(options?.search ?? '')}${options?.hash ?? ''}`);
|
|
146
152
|
}
|
|
147
153
|
|
|
@@ -170,8 +176,13 @@ export async function onNavigate(path, options = {}) {
|
|
|
170
176
|
navigationBlocker = null;
|
|
171
177
|
}
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
if (pendingController && pendingController !== currentNavigationController) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
currentNavigationController?.abort();
|
|
184
|
+
currentNavigationController = new AbortController();
|
|
185
|
+
const { signal } = currentNavigationController;
|
|
175
186
|
|
|
176
187
|
const matchPath = getMatchPath(path);
|
|
177
188
|
const { match, layouts, hooks, meta: newMeta, params: newParams } = matchRoute(matchPath, routes);
|
|
@@ -181,21 +192,23 @@ export async function onNavigate(path, options = {}) {
|
|
|
181
192
|
|
|
182
193
|
let errorHooks = [];
|
|
183
194
|
for (const hook of hooks) {
|
|
195
|
+
if (signal.aborted) return currentNavigationPromise;
|
|
184
196
|
try {
|
|
185
197
|
const { beforeLoad } = hook;
|
|
186
198
|
errorHooks.push(hook);
|
|
187
|
-
|
|
199
|
+
pendingController = currentNavigationController;
|
|
188
200
|
await beforeLoad?.(hooksContext);
|
|
189
201
|
} catch (error) {
|
|
202
|
+
if (signal.aborted) return currentNavigationPromise;
|
|
190
203
|
for (const { onError } of errorHooks) {
|
|
191
204
|
void onError?.(error, hooksContext);
|
|
192
205
|
}
|
|
193
206
|
return;
|
|
207
|
+
} finally {
|
|
208
|
+
pendingController = null;
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
211
|
|
|
197
|
-
const fromBeforeLoadHook = new Error().stack?.includes('beforeLoad');
|
|
198
|
-
|
|
199
212
|
let routeComponents;
|
|
200
213
|
try {
|
|
201
214
|
routeComponents = await resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
@@ -205,12 +218,7 @@ export async function onNavigate(path, options = {}) {
|
|
|
205
218
|
}
|
|
206
219
|
throw error;
|
|
207
220
|
}
|
|
208
|
-
if (
|
|
209
|
-
navigationIndex !== currentNavigationIndex ||
|
|
210
|
-
(fromBeforeLoadHook && pendingNavigationIndex + 1 !== currentNavigationIndex)
|
|
211
|
-
) {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
221
|
+
if (signal.aborted) return currentNavigationPromise;
|
|
214
222
|
|
|
215
223
|
if (path) {
|
|
216
224
|
const search = serializeSearch(options.search);
|
package/src/helpers/is-active.js
CHANGED
|
@@ -27,7 +27,8 @@ isActive.startsWith = (pathname, params) => {
|
|
|
27
27
|
*/
|
|
28
28
|
function compare(compareFn, pathname, params) {
|
|
29
29
|
if (!pathname.includes(':')) {
|
|
30
|
-
|
|
30
|
+
const target = base.name && base.name !== '#' ? constructPath(pathname) : pathname;
|
|
31
|
+
return compareFn(location.pathname, target);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
if (params) {
|
|
@@ -71,7 +71,6 @@ export function matchRoute(pathname, routes) {
|
|
|
71
71
|
params[param] = pathParts.slice(index).map(decodeURIComponent).join('/');
|
|
72
72
|
}
|
|
73
73
|
if (breakFromLayouts) {
|
|
74
|
-
routePart = `(${routePart})`;
|
|
75
74
|
layouts = [];
|
|
76
75
|
} else if ('layout' in routes && routes.layout) {
|
|
77
76
|
layouts.push(routes.layout);
|
|
@@ -126,6 +125,7 @@ export function matchRoute(pathname, routes) {
|
|
|
126
125
|
meta = { ...meta, ...result.meta };
|
|
127
126
|
if (result.breakFromLayouts) {
|
|
128
127
|
layouts = [];
|
|
128
|
+
breakFromLayouts = true;
|
|
129
129
|
} else {
|
|
130
130
|
layouts.push(...result.layouts);
|
|
131
131
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import type { Component, Snippet } from 'svelte';
|
|
2
2
|
import type { Action } from 'svelte/action';
|
|
3
|
+
import type { Attachment } from 'svelte/attachments';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* to `
|
|
6
|
+
* @deprecated Use the `isActiveLink` [attachment](https://svelte.dev/docs/svelte/@attach) instead.
|
|
7
|
+
*
|
|
8
|
+
* A Svelte action that will add a class to the anchor if its `href` matches the current route. It
|
|
9
|
+
* can have an optional `className` parameter to specify the class to add, otherwise it will
|
|
10
|
+
* default to `is-active`, and an optional `startsWith` parameter.
|
|
11
|
+
*
|
|
12
|
+
* ```svelte
|
|
13
|
+
* <a href={p('/about')} use:isActiveLink={{ className: 'active-link' }}>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const isActiveLinkAction: IsActiveLinkAction;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A Svelte attachment that will add a class to the anchor if its `href` matches the current route.
|
|
20
|
+
* It can have an optional `className` parameter to specify the class to add, otherwise it will
|
|
21
|
+
* default to `is-active`, and an optional `startsWith` parameter.
|
|
8
22
|
*
|
|
9
23
|
* ```svelte
|
|
10
|
-
* <a href={p('/about')}
|
|
24
|
+
* <a href={p('/about')} {@attach isActiveLink({ className: 'active-link' })}>
|
|
11
25
|
* ```
|
|
12
26
|
*/
|
|
13
27
|
export const isActiveLink: IsActiveLink;
|
|
@@ -92,7 +106,12 @@ export type Routes = {
|
|
|
92
106
|
meta?: RouteMeta;
|
|
93
107
|
};
|
|
94
108
|
|
|
95
|
-
export type IsActiveLink =
|
|
109
|
+
export type IsActiveLink = (options?: {
|
|
110
|
+
className?: string;
|
|
111
|
+
startsWith?: boolean;
|
|
112
|
+
}) => Attachment<HTMLAnchorElement>;
|
|
113
|
+
|
|
114
|
+
export type IsActiveLinkAction = Action<
|
|
96
115
|
HTMLAnchorElement,
|
|
97
116
|
| {
|
|
98
117
|
className?: string;
|
|
@@ -154,7 +173,7 @@ export type RouterApi<T extends Routes> = {
|
|
|
154
173
|
* @param options The navigation options.
|
|
155
174
|
* @returns {@link Navigation} For use with `throw navigate(...)` inside hooks.
|
|
156
175
|
*/
|
|
157
|
-
navigate<U extends Path<T>>(...args: NavigateArgs<U>): Navigation
|
|
176
|
+
navigate<U extends Path<T>>(...args: NavigateArgs<U>): Promise<Navigation>;
|
|
158
177
|
|
|
159
178
|
/**
|
|
160
179
|
* Will return `true` if the given path is active.
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { isActiveLink } from './
|
|
1
|
+
export { isActiveLink, isActiveLinkAction } from './attachments.svelte.js';
|
|
2
2
|
export { blockNavigation, createRouter } from './create-router.svelte.js';
|
|
3
3
|
export { serializeSearch } from './helpers/utils.js';
|
|
4
4
|
export { Navigation } from './navigation.js';
|
package/src/actions.svelte.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { base, location } from './create-router.svelte.js';
|
|
2
|
-
import { join } from './helpers/utils.js';
|
|
3
|
-
|
|
4
|
-
/** @type {import('./index.d.ts').IsActiveLink} */
|
|
5
|
-
export function isActiveLink(node, { className = 'is-active', startsWith = false } = {}) {
|
|
6
|
-
if (node.tagName !== 'A') {
|
|
7
|
-
throw new Error('isActiveLink can only be used on <a> elements');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
$effect(() => {
|
|
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
|
-
}
|
|
19
|
-
}
|
|
20
|
-
const tokens = className.split(' ').filter(Boolean) ?? [];
|
|
21
|
-
if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
|
|
22
|
-
node.classList.add(...tokens);
|
|
23
|
-
} else {
|
|
24
|
-
node.classList.remove(...tokens);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
}
|