sv-router 0.0.5 → 0.0.6
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 +13 -13
- package/src/actions.svelte.js +17 -0
- package/src/create-router.svelte.js +31 -20
- package/src/gen/generate-router-code.js +10 -8
- package/src/helpers/is-active.js +31 -0
- package/src/helpers/match-route.js +6 -2
- package/src/index.d.ts +52 -7
- package/src/index.js +2 -0
- package/src/search-params.svelte.js +78 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Modern Svelte routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -33,25 +33,25 @@
|
|
|
33
33
|
"src"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"esm-env": "^1.2.
|
|
36
|
+
"esm-env": "^1.2.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@changesets/cli": "^2.27.
|
|
40
|
-
"@eslint/js": "^9.
|
|
41
|
-
"@types/node": "^22.10.
|
|
42
|
-
"eslint-config-prettier": "^
|
|
39
|
+
"@changesets/cli": "^2.27.11",
|
|
40
|
+
"@eslint/js": "^9.18.0",
|
|
41
|
+
"@types/node": "^22.10.7",
|
|
42
|
+
"eslint-config-prettier": "^10.0.1",
|
|
43
43
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
44
44
|
"eslint-plugin-svelte": "^2.46.1",
|
|
45
45
|
"eslint-plugin-unicorn": "^56.0.1",
|
|
46
|
-
"globals": "^15.
|
|
46
|
+
"globals": "^15.14.0",
|
|
47
47
|
"prettier": "^3.4.2",
|
|
48
|
-
"prettier-plugin-jsdoc": "^1.3.
|
|
49
|
-
"prettier-plugin-svelte": "^3.3.
|
|
48
|
+
"prettier-plugin-jsdoc": "^1.3.2",
|
|
49
|
+
"prettier-plugin-svelte": "^3.3.3",
|
|
50
50
|
"type-testing": "^0.2.0",
|
|
51
|
-
"typescript": "^5.7.
|
|
52
|
-
"typescript-eslint": "^8.
|
|
53
|
-
"vite": "^6.0.
|
|
54
|
-
"vitest": "^
|
|
51
|
+
"typescript": "^5.7.3",
|
|
52
|
+
"typescript-eslint": "^8.20.0",
|
|
53
|
+
"vite": "^6.0.7",
|
|
54
|
+
"vitest": "^3.0.2"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"svelte": "^5"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { location } from './create-router.svelte.js';
|
|
2
|
+
|
|
3
|
+
/** @type {import('./index.d.ts').IsActiveLink} */
|
|
4
|
+
export function isActiveLink(node, { className = 'is-active' } = {}) {
|
|
5
|
+
if (node.tagName !== 'A') {
|
|
6
|
+
throw new Error('isActiveLink can only be used on <a> elements');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
$effect(() => {
|
|
10
|
+
const pathname = new URL(node.href).pathname;
|
|
11
|
+
if (pathname === location.pathname) {
|
|
12
|
+
node.classList.add(className);
|
|
13
|
+
} else {
|
|
14
|
+
node.classList.remove(className);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { BROWSER, DEV } from 'esm-env';
|
|
2
|
+
import { isActive } from './helpers/is-active.js';
|
|
2
3
|
import { matchRoute } from './helpers/match-route.js';
|
|
3
4
|
import { preloadOnHover } from './helpers/preload-on-hover.js';
|
|
4
5
|
import { constructPath, resolveRouteComponents } from './helpers/utils.js';
|
|
6
|
+
import { syncSearchParams } from './search-params.svelte.js';
|
|
5
7
|
|
|
6
8
|
/** @type {import('./index.d.ts').Routes} */
|
|
7
9
|
export let routes;
|
|
@@ -12,7 +14,7 @@ export let componentTree = $state({ value: [] });
|
|
|
12
14
|
/** @type {{ value: Record<string, string> }} */
|
|
13
15
|
export let params = $state({ value: {} });
|
|
14
16
|
|
|
15
|
-
let location = $state(updatedLocation());
|
|
17
|
+
export let location = $state(updatedLocation());
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* @template {import('./index.d.ts').Routes} T
|
|
@@ -32,24 +34,8 @@ export function createRouter(r) {
|
|
|
32
34
|
|
|
33
35
|
return {
|
|
34
36
|
p: constructPath,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
* @param {import('./index.d.ts').NavigateOptions & { params?: Record<string, string> }} options
|
|
38
|
-
*/
|
|
39
|
-
navigate(path, options = {}) {
|
|
40
|
-
if (options.params) {
|
|
41
|
-
path = constructPath(path, options.params);
|
|
42
|
-
}
|
|
43
|
-
if (options.search) {
|
|
44
|
-
path += options.search;
|
|
45
|
-
}
|
|
46
|
-
if (options.hash) {
|
|
47
|
-
path += options.hash;
|
|
48
|
-
}
|
|
49
|
-
const historyMethod = options.replace ? 'replaceState' : 'pushState';
|
|
50
|
-
globalThis.history[historyMethod](options.state || {}, '', path);
|
|
51
|
-
onNavigate();
|
|
52
|
-
},
|
|
37
|
+
navigate,
|
|
38
|
+
isActive,
|
|
53
39
|
route: {
|
|
54
40
|
get params() {
|
|
55
41
|
return params.value;
|
|
@@ -70,11 +56,36 @@ export function createRouter(r) {
|
|
|
70
56
|
};
|
|
71
57
|
}
|
|
72
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} path
|
|
61
|
+
* @param {import('./index.d.ts').NavigateOptions & { params?: Record<string, string> }} options
|
|
62
|
+
*/
|
|
63
|
+
function navigate(path, options = {}) {
|
|
64
|
+
if (options.params) {
|
|
65
|
+
path = constructPath(path, options.params);
|
|
66
|
+
}
|
|
67
|
+
if (options.search) {
|
|
68
|
+
path += (options.search.startsWith('?') ? '' : '?') + options.search;
|
|
69
|
+
}
|
|
70
|
+
if (options.hash) {
|
|
71
|
+
path += options.hash;
|
|
72
|
+
}
|
|
73
|
+
const historyMethod = options.replace ? 'replaceState' : 'pushState';
|
|
74
|
+
globalThis.history[historyMethod](options.state || {}, '', path);
|
|
75
|
+
onNavigate();
|
|
76
|
+
}
|
|
77
|
+
navigate.back = () => globalThis.history.back();
|
|
78
|
+
navigate.forward = () => globalThis.history.forward();
|
|
79
|
+
|
|
73
80
|
export function onNavigate() {
|
|
74
81
|
if (!routes) {
|
|
75
82
|
throw new Error('Router not initialized: `createRouter` was not called.');
|
|
76
83
|
}
|
|
77
|
-
|
|
84
|
+
|
|
85
|
+
syncSearchParams();
|
|
86
|
+
|
|
87
|
+
Object.assign(location, updatedLocation());
|
|
88
|
+
|
|
78
89
|
const { match, layouts, params: newParams } = matchRoute(globalThis.location.pathname, routes);
|
|
79
90
|
params.value = newParams || {};
|
|
80
91
|
resolveRouteComponents(match ? [...layouts, match] : layouts).then((components) => {
|
|
@@ -46,7 +46,8 @@ export function buildFileTree(routesPath) {
|
|
|
46
46
|
* @param {string} path
|
|
47
47
|
*/
|
|
48
48
|
function handleFlatFilename(tree, path) {
|
|
49
|
-
|
|
49
|
+
// Split the path by the first dot that is not preceded or followed by another dot
|
|
50
|
+
const splited = path.split(/(?<!\.)\.(?!\.)/);
|
|
50
51
|
if (splited.length === 2) {
|
|
51
52
|
tree.push(path);
|
|
52
53
|
return;
|
|
@@ -74,17 +75,18 @@ export function createRouteMap(fileTree, prefix = '') {
|
|
|
74
75
|
const result = {};
|
|
75
76
|
for (const entry of fileTree) {
|
|
76
77
|
if (typeof entry === 'string') {
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
const catchAll = /\[\.\.\.(.*)\]\.svelte/g.exec(entry); // Match [...slug].svelte
|
|
79
|
+
switch (true) {
|
|
80
|
+
case entry === 'index.svelte': {
|
|
79
81
|
result['/'] = prefix + entry;
|
|
80
82
|
break;
|
|
81
83
|
}
|
|
82
|
-
case '
|
|
83
|
-
result['
|
|
84
|
+
case entry === 'layout.svelte': {
|
|
85
|
+
result['layout'] = prefix + entry;
|
|
84
86
|
break;
|
|
85
87
|
}
|
|
86
|
-
case
|
|
87
|
-
result['
|
|
88
|
+
case !!catchAll: {
|
|
89
|
+
result['*' + catchAll[1]] = prefix + entry;
|
|
88
90
|
break;
|
|
89
91
|
}
|
|
90
92
|
default: {
|
|
@@ -121,6 +123,6 @@ export function createRouterCode(routes, routesPath) {
|
|
|
121
123
|
return [
|
|
122
124
|
'import { createRouter } from "sv-router";',
|
|
123
125
|
'\n\n',
|
|
124
|
-
`export const { p, navigate, route } = createRouter(${withImports});`,
|
|
126
|
+
`export const { p, navigate, isActive, route } = createRouter(${withImports});`,
|
|
125
127
|
].join('');
|
|
126
128
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { location } from '../create-router.svelte.js';
|
|
2
|
+
import { constructPath } from './utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} pathname
|
|
6
|
+
* @param {Record<string, string>} [params]
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
export function isActive(pathname, params) {
|
|
10
|
+
if (!pathname.includes(':')) {
|
|
11
|
+
return pathname === location.pathname;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (params) {
|
|
15
|
+
return constructPath(pathname, params) === location.pathname;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pathParts = pathname.split('/').slice(1);
|
|
19
|
+
const routeParts = location.pathname.split('/').slice(1);
|
|
20
|
+
if (pathParts.length !== routeParts.length) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
for (const [index, pathPart] of pathParts.entries()) {
|
|
24
|
+
const routePart = routeParts[index];
|
|
25
|
+
if (routePart.startsWith(':')) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
return pathPart === routePart;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
@@ -48,7 +48,11 @@ export function matchRoute(pathname, routes) {
|
|
|
48
48
|
const pathPart = pathParts[index];
|
|
49
49
|
if (routePart.startsWith(':')) {
|
|
50
50
|
params[routePart.slice(1)] = pathPart;
|
|
51
|
-
} else if (routePart
|
|
51
|
+
} else if (routePart.startsWith('*')) {
|
|
52
|
+
const param = routePart.slice(1);
|
|
53
|
+
if (param) {
|
|
54
|
+
params[param] = pathParts.slice(index).join('/');
|
|
55
|
+
}
|
|
52
56
|
const resolvedPath = /** @type {keyof Routes} */ (
|
|
53
57
|
(index ? '/' : '') + routeParts.join('/')
|
|
54
58
|
);
|
|
@@ -110,7 +114,7 @@ export function sortRoutes(routes) {
|
|
|
110
114
|
*/
|
|
111
115
|
function getRoutePriority(route) {
|
|
112
116
|
if (route === '' || route === '/') return 1;
|
|
113
|
-
if (route
|
|
117
|
+
if (route.startsWith('*')) return 4;
|
|
114
118
|
if (route.includes(':')) return 3;
|
|
115
119
|
return 2;
|
|
116
120
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import type { Component, Snippet } from 'svelte';
|
|
2
|
+
import type { Action } from 'svelte/action';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* A Svelte action that will add a class to the anchor if its `href` matches the current route. It
|
|
6
|
+
* can have an optional `className` parameter to specify the class to add, otherwise it will default
|
|
7
|
+
* to `is-active`.
|
|
8
|
+
*
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <a href="/about" use:isActiveLink={{ className: 'active-link' }}>
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export const isActiveLink: IsActiveLink;
|
|
3
14
|
/**
|
|
4
15
|
* Setup a new router instance with the given routes.
|
|
5
16
|
*
|
|
@@ -12,7 +23,13 @@ import type { Component, Snippet } from 'svelte';
|
|
|
12
23
|
* ```
|
|
13
24
|
*/
|
|
14
25
|
export function createRouter<T extends Routes>(r: T): RouterApi<T>;
|
|
26
|
+
/** The component that will render the current route. */
|
|
15
27
|
export const Router: Component;
|
|
28
|
+
/**
|
|
29
|
+
* The reactive search params of the URL. It is just a wrapper around `SvelteURLSearchParam` that
|
|
30
|
+
* will update the url on change.
|
|
31
|
+
*/
|
|
32
|
+
export const searchParams: URLSearchParams;
|
|
16
33
|
|
|
17
34
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
18
35
|
type BaseProps = {};
|
|
@@ -28,10 +45,12 @@ export type LayoutComponent = RouteComponent<{ children: Snippet }>;
|
|
|
28
45
|
|
|
29
46
|
export type Routes = {
|
|
30
47
|
[_: `/${string}`]: RouteComponent | Routes;
|
|
31
|
-
|
|
48
|
+
[_: `*${string}`]: RouteComponent | undefined;
|
|
32
49
|
layout?: LayoutComponent;
|
|
33
50
|
};
|
|
34
51
|
|
|
52
|
+
export type IsActiveLink = Action<HTMLAnchorElement, { className?: string } | undefined>;
|
|
53
|
+
|
|
35
54
|
export type RouterApi<T extends Routes> = {
|
|
36
55
|
/**
|
|
37
56
|
* Construct a path while ensuring type safety.
|
|
@@ -57,12 +76,29 @@ export type RouterApi<T extends Routes> = {
|
|
|
57
76
|
* id: 1,
|
|
58
77
|
* },
|
|
59
78
|
* });
|
|
79
|
+
* // Back and forward
|
|
80
|
+
* navigate.back();
|
|
81
|
+
* navigate.forward();
|
|
60
82
|
* ```
|
|
61
83
|
*
|
|
62
84
|
* @param route The route to navigate to.
|
|
63
85
|
* @param options The navigation options.
|
|
64
86
|
*/
|
|
65
|
-
navigate
|
|
87
|
+
navigate: {
|
|
88
|
+
<U extends Path<T>>(...args: NavigateArgs<U>): void;
|
|
89
|
+
back: () => void;
|
|
90
|
+
forward: () => void;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Will return `true` if the given path is active.
|
|
94
|
+
*
|
|
95
|
+
* Can be used with params to check the exact path, or without to check for any params in the
|
|
96
|
+
* path.
|
|
97
|
+
*
|
|
98
|
+
* @param path The route to check.
|
|
99
|
+
* @param params The optional parameters to replace in the route.
|
|
100
|
+
*/
|
|
101
|
+
isActive<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
|
|
66
102
|
route: {
|
|
67
103
|
/**
|
|
68
104
|
* An object containing the parameters of the current route.
|
|
@@ -90,6 +126,9 @@ export type Path<T extends Routes> = RemoveParenthesis<
|
|
|
90
126
|
export type ConstructPathArgs<T extends string> =
|
|
91
127
|
PathParams<T> extends never ? [T] : [T, PathParams<T>];
|
|
92
128
|
|
|
129
|
+
export type IsActiveArgs<T extends string> =
|
|
130
|
+
PathParams<T> extends never ? [T] : [T] | [T, PathParams<T>];
|
|
131
|
+
|
|
93
132
|
export type PathParams<T extends string> =
|
|
94
133
|
ExtractParams<T> extends never ? never : Record<ExtractParams<T>, string>;
|
|
95
134
|
|
|
@@ -104,15 +143,17 @@ export type NavigateOptions =
|
|
|
104
143
|
}
|
|
105
144
|
| undefined;
|
|
106
145
|
|
|
107
|
-
|
|
146
|
+
type NavigateArgs<T extends string> =
|
|
108
147
|
PathParams<T> extends never
|
|
109
148
|
? [T, NavigateOptions]
|
|
110
149
|
: [T, NavigateOptions & { params: PathParams<T> }];
|
|
111
150
|
|
|
112
151
|
type StripNonRoutes<T extends Routes> = {
|
|
113
|
-
[K in keyof T as K extends
|
|
114
|
-
?
|
|
115
|
-
:
|
|
152
|
+
[K in keyof T as K extends `*${string}`
|
|
153
|
+
? never
|
|
154
|
+
: K extends 'layout'
|
|
155
|
+
? never
|
|
156
|
+
: K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
|
|
116
157
|
};
|
|
117
158
|
|
|
118
159
|
type RecursiveKeys<T extends Routes, Prefix extends string = ''> = {
|
|
@@ -133,4 +174,8 @@ type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${inf
|
|
|
133
174
|
? Param | ExtractParams<`/${Rest}`>
|
|
134
175
|
: T extends `${string}:${infer Param}`
|
|
135
176
|
? Param
|
|
136
|
-
:
|
|
177
|
+
: T extends `${string}*${infer Param}`
|
|
178
|
+
? Param extends ''
|
|
179
|
+
? never
|
|
180
|
+
: Param
|
|
181
|
+
: never;
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
|
2
|
+
|
|
3
|
+
let searchParams = new SvelteURLSearchParams(globalThis.location.search);
|
|
4
|
+
|
|
5
|
+
/** @type {URLSearchParams} */
|
|
6
|
+
const shell = {
|
|
7
|
+
append(...args) {
|
|
8
|
+
searchParams.append(...args);
|
|
9
|
+
updateUrlSearchParams();
|
|
10
|
+
},
|
|
11
|
+
delete(...args) {
|
|
12
|
+
searchParams.delete(...args);
|
|
13
|
+
updateUrlSearchParams();
|
|
14
|
+
},
|
|
15
|
+
entries() {
|
|
16
|
+
return searchParams.entries();
|
|
17
|
+
},
|
|
18
|
+
forEach(...args) {
|
|
19
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
20
|
+
return searchParams.forEach(...args);
|
|
21
|
+
},
|
|
22
|
+
get(...args) {
|
|
23
|
+
return searchParams.get(...args);
|
|
24
|
+
},
|
|
25
|
+
getAll(...args) {
|
|
26
|
+
return searchParams.getAll(...args);
|
|
27
|
+
},
|
|
28
|
+
has(...args) {
|
|
29
|
+
return searchParams.has(...args);
|
|
30
|
+
},
|
|
31
|
+
keys() {
|
|
32
|
+
return searchParams.keys();
|
|
33
|
+
},
|
|
34
|
+
set(...args) {
|
|
35
|
+
searchParams.set(...args);
|
|
36
|
+
updateUrlSearchParams();
|
|
37
|
+
},
|
|
38
|
+
sort() {
|
|
39
|
+
searchParams.sort();
|
|
40
|
+
updateUrlSearchParams();
|
|
41
|
+
},
|
|
42
|
+
toString() {
|
|
43
|
+
return searchParams.toString();
|
|
44
|
+
},
|
|
45
|
+
values() {
|
|
46
|
+
return searchParams.values();
|
|
47
|
+
},
|
|
48
|
+
get size() {
|
|
49
|
+
return searchParams.size;
|
|
50
|
+
},
|
|
51
|
+
[Symbol.iterator]() {
|
|
52
|
+
return searchParams[Symbol.iterator]();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export { shell as searchParams };
|
|
57
|
+
|
|
58
|
+
export function syncSearchParams() {
|
|
59
|
+
const newSearchParams = new URLSearchParams(globalThis.location.search);
|
|
60
|
+
if (searchParams.toString() === newSearchParams.toString()) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const key of searchParams.keys()) {
|
|
65
|
+
searchParams.delete(key);
|
|
66
|
+
}
|
|
67
|
+
for (const [key, value] of newSearchParams.entries()) {
|
|
68
|
+
searchParams.append(key, value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function updateUrlSearchParams() {
|
|
73
|
+
let url = globalThis.location.origin + globalThis.location.pathname;
|
|
74
|
+
if (searchParams.size > 0) {
|
|
75
|
+
url += '?' + searchParams.toString();
|
|
76
|
+
}
|
|
77
|
+
globalThis.history.pushState({}, '', url);
|
|
78
|
+
}
|