sv-router 0.3.0 â 0.5.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
CHANGED
|
@@ -27,9 +27,15 @@ A feature-rich yet intuitive routing library for Svelte single-page apps.
|
|
|
27
27
|
- ðŠķ **Lightweight**: Minimal impact on your bundle size.
|
|
28
28
|
- ð **Made for Svelte 5**: True Svelte 5 implementation with the latest features.
|
|
29
29
|
|
|
30
|
-
##
|
|
30
|
+
## Getting Started
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
Kickstart a new project:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm create sv-router
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
...or add it to an existing project:
|
|
33
39
|
|
|
34
40
|
```bash
|
|
35
41
|
npm install sv-router
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Modern Svelte Routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -36,29 +36,31 @@
|
|
|
36
36
|
"esm-env": "^1.2.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@changesets/cli": "^2.
|
|
40
|
-
"@eslint/js": "^9.
|
|
41
|
-
"@types/node": "^22.
|
|
42
|
-
"eslint": "^9.
|
|
43
|
-
"eslint-config-prettier": "^10.1.
|
|
39
|
+
"@changesets/cli": "^2.29.3",
|
|
40
|
+
"@eslint/js": "^9.26.0",
|
|
41
|
+
"@types/node": "^22.15.15",
|
|
42
|
+
"eslint": "^9.26.0",
|
|
43
|
+
"eslint-config-prettier": "^10.1.3",
|
|
44
44
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
45
|
-
"eslint-plugin-svelte": "^3.
|
|
46
|
-
"eslint-plugin-unicorn": "^
|
|
47
|
-
"globals": "^16.
|
|
45
|
+
"eslint-plugin-svelte": "^3.5.1",
|
|
46
|
+
"eslint-plugin-unicorn": "^59.0.1",
|
|
47
|
+
"globals": "^16.1.0",
|
|
48
48
|
"prettier": "^3.5.3",
|
|
49
49
|
"prettier-plugin-jsdoc": "^1.3.2",
|
|
50
50
|
"prettier-plugin-svelte": "^3.3.3",
|
|
51
|
-
"svelte-check": "^4.1.
|
|
51
|
+
"svelte-check": "^4.1.7",
|
|
52
52
|
"type-testing": "^0.2.0",
|
|
53
|
-
"typescript": "^5.8.
|
|
54
|
-
"typescript-eslint": "^8.
|
|
55
|
-
"vite": "^6.
|
|
56
|
-
"vitest": "^3.
|
|
53
|
+
"typescript": "^5.8.3",
|
|
54
|
+
"typescript-eslint": "^8.32.0",
|
|
55
|
+
"vite": "^6.3.5",
|
|
56
|
+
"vitest": "^3.1.3"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"svelte": "^5"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
|
+
"create:dev": "pnpm --filter create-sv-router dev",
|
|
63
|
+
"create:build": "pnpm --filter create-sv-router build",
|
|
62
64
|
"docs:dev": "pnpm --filter docs dev",
|
|
63
65
|
"docs:build": "pnpm --filter docs build",
|
|
64
66
|
"docs:preview": "pnpm --filter docs preview",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
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
|
-
import { preloadOnHover } from './helpers/preload
|
|
4
|
+
import { preload, preloadOnHover } from './helpers/preload.js';
|
|
5
5
|
import { constructPath, join, resolveRouteComponents } from './helpers/utils.js';
|
|
6
6
|
import { syncSearchParams } from './search-params.svelte.js';
|
|
7
7
|
|
|
@@ -16,6 +16,9 @@ export let params = $state({ value: {} });
|
|
|
16
16
|
|
|
17
17
|
export let location = $state(updatedLocation());
|
|
18
18
|
|
|
19
|
+
let navigationIndex = 0;
|
|
20
|
+
let pendingNavigationIndex = 0;
|
|
21
|
+
|
|
19
22
|
/** @type {{ name?: string }} */
|
|
20
23
|
export const base = {
|
|
21
24
|
name: undefined,
|
|
@@ -41,6 +44,9 @@ export function createRouter(r) {
|
|
|
41
44
|
p: constructPath,
|
|
42
45
|
navigate,
|
|
43
46
|
isActive,
|
|
47
|
+
async preload(pathname) {
|
|
48
|
+
await preload(routes, pathname);
|
|
49
|
+
},
|
|
44
50
|
route: {
|
|
45
51
|
get params() {
|
|
46
52
|
return params.value;
|
|
@@ -90,6 +96,10 @@ export async function onNavigate(path, options = {}) {
|
|
|
90
96
|
if (!routes) {
|
|
91
97
|
throw new Error('Router not initialized: `createRouter` was not called.');
|
|
92
98
|
}
|
|
99
|
+
|
|
100
|
+
navigationIndex++;
|
|
101
|
+
const currentNavigationIndex = navigationIndex;
|
|
102
|
+
|
|
93
103
|
let matchPath = path || globalThis.location.pathname;
|
|
94
104
|
if (base.name && matchPath.startsWith(base.name)) {
|
|
95
105
|
matchPath = matchPath.slice(base.name.length) || '/';
|
|
@@ -98,14 +108,22 @@ export async function onNavigate(path, options = {}) {
|
|
|
98
108
|
|
|
99
109
|
for (const { beforeLoad } of hooks) {
|
|
100
110
|
try {
|
|
111
|
+
pendingNavigationIndex = currentNavigationIndex;
|
|
101
112
|
await beforeLoad?.();
|
|
102
113
|
} catch {
|
|
103
114
|
return;
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
117
|
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
const fromBeforeLoadHook = new Error().stack?.includes('beforeLoad');
|
|
119
|
+
|
|
120
|
+
const routeComponents = await resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
121
|
+
if (
|
|
122
|
+
navigationIndex !== currentNavigationIndex ||
|
|
123
|
+
(fromBeforeLoadHook && pendingNavigationIndex + 1 !== currentNavigationIndex)
|
|
124
|
+
) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
109
127
|
|
|
110
128
|
if (path) {
|
|
111
129
|
if (options.search) path += options.search;
|
|
@@ -115,6 +133,14 @@ export async function onNavigate(path, options = {}) {
|
|
|
115
133
|
globalThis.history[historyMethod](options.state || {}, '', to);
|
|
116
134
|
}
|
|
117
135
|
|
|
136
|
+
if (options.viewTransition && document.startViewTransition !== undefined) {
|
|
137
|
+
document.startViewTransition(() => {
|
|
138
|
+
componentTree.value = routeComponents;
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
componentTree.value = routeComponents;
|
|
142
|
+
}
|
|
143
|
+
params.value = newParams || {};
|
|
118
144
|
syncSearchParams();
|
|
119
145
|
Object.assign(location, updatedLocation());
|
|
120
146
|
|
|
@@ -139,13 +165,14 @@ export function onGlobalClick(event) {
|
|
|
139
165
|
if (url.origin !== currentOrigin) return;
|
|
140
166
|
|
|
141
167
|
event.preventDefault();
|
|
142
|
-
const { replace, state, scrollToTop } = anchor.dataset;
|
|
168
|
+
const { replace, state, scrollToTop, viewTransition } = anchor.dataset;
|
|
143
169
|
onNavigate(url.pathname, {
|
|
144
170
|
replace: replace === '' || replace === 'true',
|
|
145
171
|
search: url.search,
|
|
146
172
|
state,
|
|
147
173
|
hash: url.hash,
|
|
148
174
|
scrollToTop: scrollToTop === 'false' ? false : /** @type ScrollBehavior */ (scrollToTop),
|
|
175
|
+
viewTransition: viewTransition === '' || viewTransition === 'true',
|
|
149
176
|
});
|
|
150
177
|
}
|
|
151
178
|
|
|
@@ -163,7 +163,7 @@ export function createRouterCode(routes, routesPath, { allLazy = false } = {}) {
|
|
|
163
163
|
`import { createRouter } from 'sv-router';`,
|
|
164
164
|
...imports,
|
|
165
165
|
'',
|
|
166
|
-
`export const { p, navigate, isActive, route } = createRouter(${stringifiedRoutes});`,
|
|
166
|
+
`export const { p, navigate, isActive, preload, route } = createRouter(${stringifiedRoutes});`,
|
|
167
167
|
].join('\n');
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { matchRoute } from './match-route.js';
|
|
2
2
|
import { resolveRouteComponents } from './utils.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @param {import('../index.js').Routes} routes
|
|
6
|
+
* @param {string} path
|
|
7
|
+
*/
|
|
8
|
+
export async function preload(routes, path) {
|
|
9
|
+
const { match, layouts, hooks } = matchRoute(path, routes);
|
|
10
|
+
for (const { onPreload } of hooks) {
|
|
11
|
+
onPreload?.();
|
|
12
|
+
}
|
|
13
|
+
await resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
14
|
+
}
|
|
15
|
+
|
|
4
16
|
const linkSet = new Set();
|
|
5
17
|
|
|
6
|
-
/** @param {import('../index.
|
|
18
|
+
/** @param {import('../index.js').Routes} routes */
|
|
7
19
|
export function preloadOnHover(routes) {
|
|
8
20
|
const observer = new MutationObserver(() => {
|
|
9
21
|
const links = document.querySelectorAll('a[data-preload]');
|
|
@@ -15,8 +27,7 @@ export function preloadOnHover(routes) {
|
|
|
15
27
|
link.removeEventListener('mouseenter', callback);
|
|
16
28
|
const href = link.getAttribute('href');
|
|
17
29
|
if (!href) return;
|
|
18
|
-
|
|
19
|
-
resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
30
|
+
preload(routes, href);
|
|
20
31
|
});
|
|
21
32
|
}
|
|
22
33
|
});
|
package/src/index.d.ts
CHANGED
|
@@ -7,10 +7,11 @@ import type { Action } from 'svelte/action';
|
|
|
7
7
|
* to `is-active`.
|
|
8
8
|
*
|
|
9
9
|
* ```svelte
|
|
10
|
-
* <a href=
|
|
10
|
+
* <a href={p('/about')} use:isActiveLink={{ className: 'active-link' }}>
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
13
|
export const isActiveLink: IsActiveLink;
|
|
14
|
+
|
|
14
15
|
/**
|
|
15
16
|
* Setup a new router instance with the given routes.
|
|
16
17
|
*
|
|
@@ -23,11 +24,13 @@ export const isActiveLink: IsActiveLink;
|
|
|
23
24
|
* ```
|
|
24
25
|
*/
|
|
25
26
|
export function createRouter<T extends Routes>(r: T): RouterApi<T>;
|
|
27
|
+
|
|
26
28
|
/**
|
|
27
29
|
* The component that will render the current route. You can pass a `base` prop to set the base path
|
|
28
30
|
* that is prepended to every url.
|
|
29
31
|
*/
|
|
30
32
|
export const Router: Component<{ base?: string }>;
|
|
33
|
+
|
|
31
34
|
/**
|
|
32
35
|
* The reactive search params of the URL. It is just a wrapper around `SvelteURLSearchParam` that
|
|
33
36
|
* will update the url on change.
|
|
@@ -62,6 +65,8 @@ export type Hooks = {
|
|
|
62
65
|
beforeLoad?(): void | Promise<void>;
|
|
63
66
|
/** A function that will be called after the route is loaded. */
|
|
64
67
|
afterLoad?(): void;
|
|
68
|
+
/** A function that will be called when the route is preloaded. */
|
|
69
|
+
onPreload?(): void;
|
|
65
70
|
};
|
|
66
71
|
|
|
67
72
|
export type Routes = {
|
|
@@ -90,8 +95,9 @@ export type RouterApi<T extends Routes> = {
|
|
|
90
95
|
* @param params The parameters to replace in the route.
|
|
91
96
|
*/
|
|
92
97
|
p<U extends Path<T>>(...args: ConstructPathArgs<U>): string;
|
|
98
|
+
|
|
93
99
|
/**
|
|
94
|
-
* Navigate
|
|
100
|
+
* Navigate programmatically to a route.
|
|
95
101
|
*
|
|
96
102
|
* ```js
|
|
97
103
|
* navigate('/users');
|
|
@@ -124,6 +130,14 @@ export type RouterApi<T extends Routes> = {
|
|
|
124
130
|
<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
|
|
125
131
|
startsWith<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
|
|
126
132
|
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Preloads the given route.
|
|
136
|
+
*
|
|
137
|
+
* @param path The route to preload.
|
|
138
|
+
*/
|
|
139
|
+
preload<U extends Path<T>>(path: U): Promise<void>;
|
|
140
|
+
|
|
127
141
|
route: {
|
|
128
142
|
/**
|
|
129
143
|
* An object containing the parameters of the current route.
|
|
@@ -134,7 +148,7 @@ export type RouterApi<T extends Routes> = {
|
|
|
134
148
|
*/
|
|
135
149
|
params: AllParams<T>;
|
|
136
150
|
/** The reactive pathname of the URL. */
|
|
137
|
-
pathname: Path<T
|
|
151
|
+
pathname: (Path<T, true> & {}) | (string & {});
|
|
138
152
|
/** The reactive query string part of the URL. */
|
|
139
153
|
search: string;
|
|
140
154
|
/** The reactive history state that can be passed to the `navigate` function. */
|
|
@@ -144,8 +158,8 @@ export type RouterApi<T extends Routes> = {
|
|
|
144
158
|
};
|
|
145
159
|
};
|
|
146
160
|
|
|
147
|
-
export type Path<T extends Routes> = RemoveParenthesis<
|
|
148
|
-
RemoveLastSlash<RecursiveKeys<StripNonRoutes<T
|
|
161
|
+
export type Path<T extends Routes, AnyParam extends boolean = false> = RemoveParenthesis<
|
|
162
|
+
RemoveLastSlash<RecursiveKeys<StripNonRoutes<T>, '', AnyParam>>
|
|
149
163
|
>;
|
|
150
164
|
|
|
151
165
|
export type ConstructPathArgs<T extends string> =
|
|
@@ -155,9 +169,13 @@ export type IsActiveArgs<T extends string> =
|
|
|
155
169
|
PathParams<T> extends never ? [T] : [T] | [T, PathParams<T>];
|
|
156
170
|
|
|
157
171
|
export type PathParams<T extends string> =
|
|
158
|
-
ExtractParams<T
|
|
172
|
+
ExtractParams<RemoveParenthesis<T>> extends never
|
|
173
|
+
? never
|
|
174
|
+
: Record<ExtractParams<RemoveParenthesis<T>>, string>;
|
|
159
175
|
|
|
160
|
-
export type AllParams<T extends Routes> = Partial<
|
|
176
|
+
export type AllParams<T extends Routes> = Partial<
|
|
177
|
+
Record<ExtractParams<RemoveParenthesis<RecursiveKeys<T>>>, string>
|
|
178
|
+
>;
|
|
161
179
|
|
|
162
180
|
export type NavigateOptions =
|
|
163
181
|
| {
|
|
@@ -166,6 +184,7 @@ export type NavigateOptions =
|
|
|
166
184
|
state?: string;
|
|
167
185
|
hash?: string;
|
|
168
186
|
scrollToTop?: ScrollBehavior | false;
|
|
187
|
+
viewTransition?: boolean;
|
|
169
188
|
}
|
|
170
189
|
| undefined;
|
|
171
190
|
|
|
@@ -194,14 +213,28 @@ type StripNonRoutes<T extends Routes> = {
|
|
|
194
213
|
: K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
|
|
195
214
|
};
|
|
196
215
|
|
|
197
|
-
type RecursiveKeys<
|
|
216
|
+
type RecursiveKeys<
|
|
217
|
+
T extends Routes,
|
|
218
|
+
Prefix extends string = '',
|
|
219
|
+
AnyParam extends boolean = false,
|
|
220
|
+
> = {
|
|
198
221
|
[K in keyof T]: K extends string
|
|
199
222
|
? T[K] extends Routes
|
|
200
|
-
? RecursiveKeys<
|
|
201
|
-
|
|
223
|
+
? RecursiveKeys<
|
|
224
|
+
T[K],
|
|
225
|
+
`${Prefix}${AnyParam extends true ? ReplaceParamWithString<K> : K}`,
|
|
226
|
+
AnyParam
|
|
227
|
+
>
|
|
228
|
+
: `${Prefix}${AnyParam extends true ? ReplaceParamWithString<K> : K}`
|
|
202
229
|
: never;
|
|
203
230
|
}[keyof T];
|
|
204
231
|
|
|
232
|
+
type ReplaceParamWithString<T extends string> = T extends `/:${string}`
|
|
233
|
+
? `/${string}`
|
|
234
|
+
: T extends `/(:${string})`
|
|
235
|
+
? `/${string}`
|
|
236
|
+
: T;
|
|
237
|
+
|
|
205
238
|
type RemoveLastSlash<T extends string> = T extends '/' ? T : T extends `${infer R}/` ? R : T;
|
|
206
239
|
|
|
207
240
|
type RemoveParenthesis<T extends string> = T extends `${infer A}(${infer B})${infer C}`
|
|
@@ -210,14 +243,10 @@ type RemoveParenthesis<T extends string> = T extends `${infer A}(${infer B})${in
|
|
|
210
243
|
|
|
211
244
|
type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
|
|
212
245
|
? Param | ExtractParams<`/${Rest}`>
|
|
213
|
-
: T extends `${string}
|
|
246
|
+
: T extends `${string}:${infer Param}`
|
|
214
247
|
? Param
|
|
215
|
-
: T extends `${string}
|
|
216
|
-
? Param
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
? Param extends ''
|
|
221
|
-
? never
|
|
222
|
-
: Param
|
|
223
|
-
: never;
|
|
248
|
+
: T extends `${string}*${infer Param}`
|
|
249
|
+
? Param extends ''
|
|
250
|
+
? never
|
|
251
|
+
: Param
|
|
252
|
+
: never;
|
|
@@ -7,9 +7,9 @@ import { writeRouterCode } from '../gen/write-router-code.js';
|
|
|
7
7
|
* @returns {import('vite').Plugin}
|
|
8
8
|
*/
|
|
9
9
|
export function router(options) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
genConfig.allLazy = options?.allLazy || false;
|
|
11
|
+
genConfig.routesInJs = options?.js || false;
|
|
12
|
+
genConfig.routesPath = options?.path || 'src/routes';
|
|
13
13
|
|
|
14
14
|
return {
|
|
15
15
|
name: 'sv-router',
|