sv-router 0.7.1 → 0.8.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 +20 -19
- package/src/Router.svelte +2 -10
- package/src/actions.svelte.js +11 -5
- package/src/create-router.svelte.js +96 -26
- package/src/gen/generate-router-code.js +3 -1
- package/src/helpers/is-active.js +8 -4
- package/src/helpers/match-route.js +4 -2
- package/src/helpers/preload.js +2 -2
- package/src/helpers/utils.js +35 -8
- package/src/index.d.ts +47 -16
- package/src/search-params.svelte.js +11 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Modern Svelte Routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -36,28 +36,29 @@
|
|
|
36
36
|
"esm-env": "^1.2.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@changesets/cli": "^2.29.
|
|
40
|
-
"@eslint/js": "^9.
|
|
41
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
42
|
-
"@testing-library/jest-dom": "^6.6.
|
|
39
|
+
"@changesets/cli": "^2.29.5",
|
|
40
|
+
"@eslint/js": "^9.32.0",
|
|
41
|
+
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
|
42
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
43
43
|
"@testing-library/svelte": "^5.2.8",
|
|
44
|
-
"@
|
|
45
|
-
"
|
|
46
|
-
"eslint
|
|
44
|
+
"@testing-library/user-event": "^14.6.1",
|
|
45
|
+
"@types/node": "^24.2.0",
|
|
46
|
+
"eslint": "^9.32.0",
|
|
47
|
+
"eslint-config-prettier": "^10.1.8",
|
|
47
48
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
48
|
-
"eslint-plugin-svelte": "^3.
|
|
49
|
-
"eslint-plugin-unicorn": "^
|
|
50
|
-
"globals": "^16.
|
|
51
|
-
"
|
|
52
|
-
"prettier": "^3.
|
|
53
|
-
"prettier-plugin-jsdoc": "^1.3.
|
|
49
|
+
"eslint-plugin-svelte": "^3.11.0",
|
|
50
|
+
"eslint-plugin-unicorn": "^60.0.0",
|
|
51
|
+
"globals": "^16.3.0",
|
|
52
|
+
"happy-dom": "^18.0.1",
|
|
53
|
+
"prettier": "^3.6.2",
|
|
54
|
+
"prettier-plugin-jsdoc": "^1.3.3",
|
|
54
55
|
"prettier-plugin-svelte": "^3.4.0",
|
|
55
|
-
"svelte-check": "^4.
|
|
56
|
+
"svelte-check": "^4.3.1",
|
|
56
57
|
"type-testing": "^0.2.0",
|
|
57
|
-
"typescript": "^5.
|
|
58
|
-
"typescript-eslint": "^8.
|
|
59
|
-
"vite": "^7.
|
|
60
|
-
"vitest": "^3.2.
|
|
58
|
+
"typescript": "^5.9.2",
|
|
59
|
+
"typescript-eslint": "^8.39.0",
|
|
60
|
+
"vite": "^7.1.0",
|
|
61
|
+
"vitest": "^3.2.4"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
64
|
"svelte": "^5"
|
package/src/Router.svelte
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { on } from 'svelte/events';
|
|
3
|
-
import {
|
|
4
|
-
import { join } from './helpers/utils.js';
|
|
3
|
+
import { componentTree, init, onGlobalClick, onNavigate } from './create-router.svelte.js';
|
|
5
4
|
import RecursiveComponentTree from './RecursiveComponentTree.svelte';
|
|
6
5
|
|
|
7
6
|
/** @type {{ base?: string }} */
|
|
8
7
|
let { base: basename } = $props();
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
base.name = (basename.startsWith('/') ? '' : '/') + basename;
|
|
12
|
-
const url = new URL(globalThis.location.href);
|
|
13
|
-
if (!url.pathname.startsWith(base.name)) {
|
|
14
|
-
url.pathname = join(base.name, url.pathname);
|
|
15
|
-
history.replaceState(history.state || {}, '', url.href);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
9
|
+
init(basename);
|
|
18
10
|
|
|
19
11
|
onNavigate();
|
|
20
12
|
|
package/src/actions.svelte.js
CHANGED
|
@@ -8,14 +8,20 @@ export function isActiveLink(node, { className = 'is-active', startsWith = false
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
$effect(() => {
|
|
11
|
-
let pathname
|
|
12
|
-
if (base.name) {
|
|
13
|
-
pathname =
|
|
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
|
+
}
|
|
14
19
|
}
|
|
20
|
+
const tokens = className.split(' ').filter(Boolean) ?? [];
|
|
15
21
|
if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
|
|
16
|
-
node.classList.add(
|
|
22
|
+
node.classList.add(...tokens);
|
|
17
23
|
} else {
|
|
18
|
-
node.classList.remove(
|
|
24
|
+
node.classList.remove(...tokens);
|
|
19
25
|
}
|
|
20
26
|
});
|
|
21
27
|
}
|
|
@@ -2,12 +2,23 @@ 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 {
|
|
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} */
|
|
9
15
|
let routes;
|
|
10
16
|
|
|
17
|
+
/** @type {{ name?: string }} */
|
|
18
|
+
export const base = {
|
|
19
|
+
name: undefined,
|
|
20
|
+
};
|
|
21
|
+
|
|
11
22
|
/** @type {{ value: import('svelte').Component[] }} */
|
|
12
23
|
export let componentTree = $state({ value: [] });
|
|
13
24
|
|
|
@@ -21,10 +32,26 @@ let meta = $state({ value: {} });
|
|
|
21
32
|
let navigationIndex = 0;
|
|
22
33
|
let pendingNavigationIndex = 0;
|
|
23
34
|
|
|
24
|
-
/** @
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
/** @param {string | undefined} basename */
|
|
36
|
+
export function init(basename) {
|
|
37
|
+
if (basename) {
|
|
38
|
+
const url = new URL(globalThis.location.toString());
|
|
39
|
+
if (basename === '#') {
|
|
40
|
+
base.name = '#';
|
|
41
|
+
if (!globalThis.location.href.includes('#')) {
|
|
42
|
+
url.hash = '/';
|
|
43
|
+
history.replaceState(history.state || {}, '', url.toString());
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
base.name = (basename.startsWith('/') ? '' : '/') + basename;
|
|
47
|
+
if (!url.pathname.startsWith(base.name)) {
|
|
48
|
+
url.pathname = join(base.name, url.pathname);
|
|
49
|
+
history.replaceState(history.state || {}, '', url.toString());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
Object.assign(location, updatedLocation());
|
|
54
|
+
}
|
|
28
55
|
|
|
29
56
|
/**
|
|
30
57
|
* @template {import('./index.d.ts').Routes} T
|
|
@@ -60,7 +87,7 @@ export function createRouter(r) {
|
|
|
60
87
|
return params.value;
|
|
61
88
|
},
|
|
62
89
|
get pathname() {
|
|
63
|
-
return /** @type {import('./index.d.ts').Path<T>} */ (location.pathname);
|
|
90
|
+
return /** @type {import('./index.d.ts').Path<T>} */ (stripBase(location.pathname));
|
|
64
91
|
},
|
|
65
92
|
get search() {
|
|
66
93
|
return location.search;
|
|
@@ -87,18 +114,39 @@ function navigate(path, options = {}) {
|
|
|
87
114
|
globalThis.history.go(path);
|
|
88
115
|
return;
|
|
89
116
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
117
|
+
|
|
118
|
+
path = constructPath(path, options.params);
|
|
93
119
|
if (options.search && !options.search.startsWith('?')) {
|
|
94
120
|
options.search = '?' + options.search;
|
|
95
121
|
}
|
|
96
|
-
if (options.hash && !options.hash.startsWith('#')) {
|
|
122
|
+
if (options.hash && !options.hash.startsWith('#') && base.name !== '#') {
|
|
97
123
|
options.hash = '#' + options.hash;
|
|
98
124
|
}
|
|
125
|
+
if (base.name === '#') {
|
|
126
|
+
path = new URL(path).hash;
|
|
127
|
+
}
|
|
99
128
|
onNavigate(path, options);
|
|
100
129
|
}
|
|
101
130
|
|
|
131
|
+
/** @param {string} [path] */
|
|
132
|
+
function getMatchPath(path) {
|
|
133
|
+
let matchPath = '';
|
|
134
|
+
|
|
135
|
+
if (path) {
|
|
136
|
+
matchPath = path;
|
|
137
|
+
} else if (base.name === '#') {
|
|
138
|
+
matchPath = globalThis.location.hash.slice(1);
|
|
139
|
+
} else {
|
|
140
|
+
matchPath = globalThis.location.pathname;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (base.name && matchPath.startsWith(base.name)) {
|
|
144
|
+
matchPath = matchPath.slice(base.name.length) || '/';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return stripBase(matchPath);
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
/**
|
|
103
151
|
* @param {string} [path]
|
|
104
152
|
* @param {import('./index.d.ts').NavigateOptions} options
|
|
@@ -111,24 +159,35 @@ export async function onNavigate(path, options = {}) {
|
|
|
111
159
|
navigationIndex++;
|
|
112
160
|
const currentNavigationIndex = navigationIndex;
|
|
113
161
|
|
|
114
|
-
let matchPath = path
|
|
115
|
-
if (base.name && matchPath.startsWith(base.name)) {
|
|
116
|
-
matchPath = matchPath.slice(base.name.length) || '/';
|
|
117
|
-
}
|
|
162
|
+
let matchPath = getMatchPath(path);
|
|
118
163
|
const { match, layouts, hooks, meta: newMeta, params: newParams } = matchRoute(matchPath, routes);
|
|
119
164
|
|
|
120
|
-
|
|
165
|
+
let errorHooks = [];
|
|
166
|
+
for (const hook of hooks) {
|
|
121
167
|
try {
|
|
168
|
+
const { beforeLoad } = hook;
|
|
169
|
+
errorHooks.push(hook);
|
|
122
170
|
pendingNavigationIndex = currentNavigationIndex;
|
|
123
|
-
await beforeLoad?.({ pathname: matchPath, ...options });
|
|
124
|
-
} catch {
|
|
171
|
+
await beforeLoad?.({ pathname: matchPath, meta: newMeta, ...options });
|
|
172
|
+
} catch (error) {
|
|
173
|
+
for (const { onError } of errorHooks) {
|
|
174
|
+
void onError?.(error, { pathname: matchPath, meta: newMeta, ...options });
|
|
175
|
+
}
|
|
125
176
|
return;
|
|
126
177
|
}
|
|
127
178
|
}
|
|
128
179
|
|
|
129
180
|
const fromBeforeLoadHook = new Error().stack?.includes('beforeLoad');
|
|
130
181
|
|
|
131
|
-
|
|
182
|
+
let routeComponents;
|
|
183
|
+
try {
|
|
184
|
+
routeComponents = await resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
for (const { onError } of hooks) {
|
|
187
|
+
void onError?.(error, { pathname: matchPath, meta: newMeta, ...options });
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
132
191
|
if (
|
|
133
192
|
navigationIndex !== currentNavigationIndex ||
|
|
134
193
|
(fromBeforeLoadHook && pendingNavigationIndex + 1 !== currentNavigationIndex)
|
|
@@ -137,11 +196,20 @@ export async function onNavigate(path, options = {}) {
|
|
|
137
196
|
}
|
|
138
197
|
|
|
139
198
|
if (path) {
|
|
140
|
-
|
|
141
|
-
|
|
199
|
+
let url = new URL(globalThis.location.toString());
|
|
200
|
+
url.search = '';
|
|
201
|
+
if (options.search) url.search = options.search;
|
|
202
|
+
if (base.name === '#') {
|
|
203
|
+
url.hash = path;
|
|
204
|
+
} else {
|
|
205
|
+
if (options.hash) path += options.hash;
|
|
206
|
+
url.pathname = base.name ? join(base.name, path) : path;
|
|
207
|
+
}
|
|
142
208
|
const historyMethod = options.replace ? 'replaceState' : 'pushState';
|
|
143
|
-
|
|
144
|
-
|
|
209
|
+
globalThis.history[historyMethod](options.state || {}, '', url.toString());
|
|
210
|
+
syncSearchParams(options.search);
|
|
211
|
+
} else {
|
|
212
|
+
syncSearchParams(globalThis.location.search);
|
|
145
213
|
}
|
|
146
214
|
|
|
147
215
|
if (options.viewTransition && document.startViewTransition !== undefined) {
|
|
@@ -153,7 +221,6 @@ export async function onNavigate(path, options = {}) {
|
|
|
153
221
|
}
|
|
154
222
|
params.value = newParams;
|
|
155
223
|
meta.value = newMeta;
|
|
156
|
-
syncSearchParams();
|
|
157
224
|
Object.assign(location, updatedLocation());
|
|
158
225
|
|
|
159
226
|
if (options.scrollToTop !== false) {
|
|
@@ -161,7 +228,7 @@ export async function onNavigate(path, options = {}) {
|
|
|
161
228
|
}
|
|
162
229
|
|
|
163
230
|
for (const { afterLoad } of hooks) {
|
|
164
|
-
afterLoad?.({ pathname: matchPath, ...options });
|
|
231
|
+
void afterLoad?.({ pathname: matchPath, meta: newMeta, ...options });
|
|
165
232
|
}
|
|
166
233
|
}
|
|
167
234
|
|
|
@@ -176,13 +243,16 @@ export function onGlobalClick(event) {
|
|
|
176
243
|
const currentOrigin = globalThis.location.origin;
|
|
177
244
|
if (url.origin !== currentOrigin) return;
|
|
178
245
|
|
|
246
|
+
const path = base.name === '#' ? url.hash : url.pathname;
|
|
247
|
+
const hash = base.name === '#' ? undefined : url.hash;
|
|
248
|
+
|
|
179
249
|
event.preventDefault();
|
|
180
250
|
const { replace, state, scrollToTop, viewTransition } = anchor.dataset;
|
|
181
|
-
onNavigate(
|
|
251
|
+
onNavigate(path, {
|
|
182
252
|
replace: replace === '' || replace === 'true',
|
|
183
253
|
search: url.search,
|
|
184
254
|
state,
|
|
185
|
-
hash
|
|
255
|
+
hash,
|
|
186
256
|
scrollToTop: scrollToTop === 'false' ? false : /** @type ScrollBehavior */ (scrollToTop),
|
|
187
257
|
viewTransition: viewTransition === '' || viewTransition === 'true',
|
|
188
258
|
});
|
|
@@ -177,7 +177,9 @@ export function createRouterCode(routes, routesPath, { allLazy = false } = {}) {
|
|
|
177
177
|
`import { createRouter } from 'sv-router';`,
|
|
178
178
|
...imports,
|
|
179
179
|
'',
|
|
180
|
-
`
|
|
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
|
|
package/src/helpers/is-active.js
CHANGED
|
@@ -7,7 +7,7 @@ import { constructPath, join } from './utils.js';
|
|
|
7
7
|
* @returns {boolean}
|
|
8
8
|
*/
|
|
9
9
|
export function isActive(pathname, params) {
|
|
10
|
-
const p = base.name ? join(base.name, pathname) : pathname;
|
|
10
|
+
const p = base.name && base.name !== '#' ? join(base.name, pathname) : pathname;
|
|
11
11
|
return compare((a, b) => a === b, p, params);
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@ export function isActive(pathname, params) {
|
|
|
17
17
|
* @returns {boolean}
|
|
18
18
|
*/
|
|
19
19
|
isActive.startsWith = (pathname, params) => {
|
|
20
|
-
const p = base.name ? join(base.name, pathname) : pathname;
|
|
20
|
+
const p = base.name && base.name !== '#' ? join(base.name, pathname) : pathname;
|
|
21
21
|
return compare((a, b) => a.startsWith(b), p, params);
|
|
22
22
|
};
|
|
23
23
|
|
|
@@ -33,11 +33,15 @@ function compare(compareFn, pathname, params) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
if (params) {
|
|
36
|
-
|
|
36
|
+
if (base.name === '#') {
|
|
37
|
+
return compareFn(location.pathname, new URL(constructPath(pathname, params)).hash.slice(1));
|
|
38
|
+
} else {
|
|
39
|
+
return compareFn(location.pathname, constructPath(pathname, params));
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
const pathParts = pathname.split('/').slice(1);
|
|
40
|
-
|
|
44
|
+
let routeParts = location.pathname.split('/').slice(1);
|
|
41
45
|
if (pathParts.length > routeParts.length) {
|
|
42
46
|
return false;
|
|
43
47
|
}
|
|
@@ -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.
|
|
146
|
+
if (route.includes('*')) return 4;
|
|
145
147
|
if (route.includes(':')) return 3;
|
|
146
148
|
return 2;
|
|
147
149
|
}
|
package/src/helpers/preload.js
CHANGED
|
@@ -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
|
}
|
package/src/helpers/utils.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
+
import { base } from '../create-router.svelte.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @param {string} path
|
|
3
5
|
* @param {Record<string, string>} [params]
|
|
4
6
|
* @returns {string}
|
|
5
7
|
*/
|
|
6
8
|
export function constructPath(path, params) {
|
|
7
|
-
if (
|
|
9
|
+
if (params) {
|
|
10
|
+
for (const key in params) {
|
|
11
|
+
path = path.replace(`:${key}`, params[key]);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
if (base.name === '#') {
|
|
16
|
+
const url = new URL(globalThis.location.toString());
|
|
17
|
+
url.hash = path;
|
|
18
|
+
url.search = '';
|
|
19
|
+
|
|
20
|
+
return url.toString();
|
|
12
21
|
}
|
|
13
|
-
|
|
22
|
+
|
|
23
|
+
return path;
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
/**
|
|
@@ -42,7 +52,10 @@ function resolveRouteComponent(input) {
|
|
|
42
52
|
* @returns {input is import('../index.d.ts').LazyRouteComponent}
|
|
43
53
|
*/
|
|
44
54
|
export function isLazyImport(input) {
|
|
45
|
-
return
|
|
55
|
+
return (
|
|
56
|
+
typeof input === 'function' &&
|
|
57
|
+
!!/\(\)\s?=>\s?(import|__vite_ssr_dynamic_import__)\(.*\)/.test(String(input))
|
|
58
|
+
);
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
/** @param {...string} parts */
|
|
@@ -60,11 +73,25 @@ export function join(...parts) {
|
|
|
60
73
|
return result;
|
|
61
74
|
}
|
|
62
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} pathname
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export function stripBase(pathname) {
|
|
81
|
+
if (base.name && pathname.startsWith(base.name)) {
|
|
82
|
+
pathname = pathname.slice(base.name.length) || '/';
|
|
83
|
+
}
|
|
84
|
+
return pathname;
|
|
85
|
+
}
|
|
86
|
+
|
|
63
87
|
export function updatedLocation() {
|
|
88
|
+
const pathname =
|
|
89
|
+
base.name === '#' ? globalThis.location.hash.slice(1) : globalThis.location.pathname;
|
|
90
|
+
const hash = base.name === '#' ? '' : globalThis.location.hash;
|
|
64
91
|
return {
|
|
65
|
-
pathname
|
|
92
|
+
pathname,
|
|
66
93
|
search: globalThis.location.search,
|
|
67
94
|
state: history.state,
|
|
68
|
-
hash
|
|
95
|
+
hash,
|
|
69
96
|
};
|
|
70
97
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -41,11 +41,12 @@ export const searchParams: SearchParams;
|
|
|
41
41
|
type BaseProps = {};
|
|
42
42
|
|
|
43
43
|
export type LazyRouteComponent<Props extends BaseProps = BaseProps> = () => Promise<{
|
|
44
|
-
default: Component<Props>;
|
|
44
|
+
default: Component<Props> | Snippet<[Props]>;
|
|
45
45
|
}>;
|
|
46
46
|
|
|
47
47
|
export type RouteComponent<Props extends BaseProps = any> =
|
|
48
48
|
| Component<Props>
|
|
49
|
+
| Snippet<[Props]>
|
|
49
50
|
| LazyRouteComponent<Props>;
|
|
50
51
|
export type LayoutComponent = RouteComponent<{ children: Snippet }>;
|
|
51
52
|
export type Hooks = {
|
|
@@ -64,9 +65,11 @@ export type Hooks = {
|
|
|
64
65
|
*/
|
|
65
66
|
beforeLoad?(context: HooksContext): void | Promise<void>;
|
|
66
67
|
/** A function that will be called after the route is loaded. */
|
|
67
|
-
afterLoad?(context: HooksContext): void
|
|
68
|
+
afterLoad?(context: HooksContext): void | Promise<void>;
|
|
68
69
|
/** A function that will be called when the route is preloaded. */
|
|
69
|
-
onPreload?(context: HooksContext): void
|
|
70
|
+
onPreload?(context: HooksContext): void | Promise<void>;
|
|
71
|
+
/** A function that will be called when the route fails to load. */
|
|
72
|
+
onError?(error: unknown, context: HooksContext): void | Promise<void>;
|
|
70
73
|
};
|
|
71
74
|
|
|
72
75
|
export type Routes = {
|
|
@@ -79,7 +82,11 @@ export type Routes = {
|
|
|
79
82
|
|
|
80
83
|
export type IsActiveLink = Action<
|
|
81
84
|
HTMLAnchorElement,
|
|
82
|
-
|
|
85
|
+
| {
|
|
86
|
+
className?: string;
|
|
87
|
+
startsWith?: boolean;
|
|
88
|
+
}
|
|
89
|
+
| undefined
|
|
83
90
|
>;
|
|
84
91
|
|
|
85
92
|
/**
|
|
@@ -143,7 +150,7 @@ export type RouterApi<T extends Routes> = {
|
|
|
143
150
|
*/
|
|
144
151
|
isActive: {
|
|
145
152
|
<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
|
|
146
|
-
startsWith<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
|
|
153
|
+
startsWith<U extends Path<T>>(...args: IsActiveArgs<U, true>): boolean;
|
|
147
154
|
};
|
|
148
155
|
|
|
149
156
|
/**
|
|
@@ -190,27 +197,39 @@ export type Path<T extends Routes, AnyParam extends boolean = false> = RemovePar
|
|
|
190
197
|
RemoveLastSlash<RecursiveKeys<StripNonRoutes<T>, '', AnyParam>>
|
|
191
198
|
>;
|
|
192
199
|
|
|
193
|
-
export type ConstructPathArgs<
|
|
194
|
-
PathParams<
|
|
200
|
+
export type ConstructPathArgs<TPath extends string> = {
|
|
201
|
+
[Path in TPath]: PathParams<Path> extends never ? [Path] : [Path, PathParams<Path>];
|
|
202
|
+
}[TPath];
|
|
195
203
|
|
|
196
|
-
export type IsActiveArgs<
|
|
197
|
-
|
|
204
|
+
export type IsActiveArgs<
|
|
205
|
+
TPath extends string,
|
|
206
|
+
StartsWith extends boolean = false,
|
|
207
|
+
> = StartsWith extends true
|
|
208
|
+
? {
|
|
209
|
+
[Path in TPath]: PathParams<Path> extends never
|
|
210
|
+
? [PathPrefixes<Path>]
|
|
211
|
+
: [PathPrefixes<Path>] | [PathPrefixes<Path>, PathParams<Path>];
|
|
212
|
+
}[TPath]
|
|
213
|
+
: {
|
|
214
|
+
[Path in TPath]: PathParams<Path> extends never ? [Path] : [Path] | [Path, PathParams<Path>];
|
|
215
|
+
}[TPath];
|
|
198
216
|
|
|
199
|
-
export type PathParams<
|
|
200
|
-
ExtractParams<RemoveParenthesis<
|
|
217
|
+
export type PathParams<TPath extends string> =
|
|
218
|
+
ExtractParams<RemoveParenthesis<TPath>> extends never
|
|
201
219
|
? never
|
|
202
|
-
: Record<ExtractParams<RemoveParenthesis<
|
|
220
|
+
: Record<ExtractParams<RemoveParenthesis<TPath>>, string>;
|
|
203
221
|
|
|
204
|
-
export type AllParams<
|
|
205
|
-
Record<ExtractParams<RemoveParenthesis<RecursiveKeys<
|
|
222
|
+
export type AllParams<TRoutes extends Routes> = Partial<
|
|
223
|
+
Record<ExtractParams<RemoveParenthesis<RecursiveKeys<TRoutes>>>, string>
|
|
206
224
|
>;
|
|
207
225
|
|
|
208
226
|
export type HooksContext = {
|
|
227
|
+
hash?: string;
|
|
228
|
+
meta: RouteMeta;
|
|
209
229
|
pathname: string;
|
|
210
230
|
replace?: boolean;
|
|
211
231
|
search?: string;
|
|
212
232
|
state?: string;
|
|
213
|
-
hash?: string;
|
|
214
233
|
};
|
|
215
234
|
|
|
216
235
|
export type NavigateOptions =
|
|
@@ -224,7 +243,7 @@ export type NavigateOptions =
|
|
|
224
243
|
}
|
|
225
244
|
| undefined;
|
|
226
245
|
|
|
227
|
-
export type SearchParams = URLSearchParams & {
|
|
246
|
+
export type SearchParams = Omit<URLSearchParams, 'append' | 'delete' | 'set' | 'sort'> & {
|
|
228
247
|
append: (name: string, value: string, options?: { replace?: boolean }) => void;
|
|
229
248
|
delete: (name: string, value?: string, options?: { replace?: boolean }) => void;
|
|
230
249
|
set: (name: string, value: string, options?: { replace?: boolean }) => void;
|
|
@@ -288,3 +307,15 @@ type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${inf
|
|
|
288
307
|
? never
|
|
289
308
|
: Param
|
|
290
309
|
: never;
|
|
310
|
+
|
|
311
|
+
type PathPrefixes<T extends string, Acc extends string = ''> = T extends '/'
|
|
312
|
+
? '/'
|
|
313
|
+
: T extends `/${infer Segment}/${infer Rest}`
|
|
314
|
+
? Acc extends ''
|
|
315
|
+
? PathPrefixes<`/${Rest}`, `/${Segment}`> | `/${Segment}`
|
|
316
|
+
: PathPrefixes<`/${Rest}`, `${Acc}/${Segment}`> | `${Acc}/${Segment}` | Acc
|
|
317
|
+
: T extends `/${infer Segment}`
|
|
318
|
+
? Acc extends ''
|
|
319
|
+
? `/${Segment}`
|
|
320
|
+
: `${Acc}/${Segment}` | Acc
|
|
321
|
+
: Acc;
|
|
@@ -55,22 +55,21 @@ const shell = {
|
|
|
55
55
|
|
|
56
56
|
export { shell as searchParams };
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (searchParams.toString()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
/** @param {string} [search] */
|
|
59
|
+
export function syncSearchParams(search) {
|
|
60
|
+
if (searchParams.toString() !== search) {
|
|
61
|
+
searchParams = new SvelteURLSearchParams();
|
|
62
|
+
const newSearchParams = new URLSearchParams(search);
|
|
63
|
+
for (const [key, value] of newSearchParams.entries()) {
|
|
64
|
+
searchParams.append(key, value);
|
|
65
|
+
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** @param {{ replace?: boolean }} [options] */
|
|
70
70
|
function updateUrlSearchParams(options) {
|
|
71
|
-
let url =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
71
|
+
let url = new URL(globalThis.location.toString());
|
|
72
|
+
url.search = searchParams.toString();
|
|
73
|
+
|
|
75
74
|
globalThis.history[options?.replace ? 'replaceState' : 'pushState']({}, '', url);
|
|
76
75
|
}
|