sv-router 0.4.0 â 0.6.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 +8 -2
- package/package.json +17 -15
- package/src/create-router.svelte.js +18 -3
- package/src/gen/generate-router-code.js +21 -5
- package/src/helpers/is-active.js +5 -3
- package/src/helpers/match-route.js +13 -2
- package/src/helpers/{preload-on-hover.js â preload.js} +14 -3
- package/src/helpers/utils.js +1 -1
- package/src/helpers/validate-routes.js +4 -1
- package/src/index.d.ts +79 -21
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.6.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.4",
|
|
40
|
+
"@eslint/js": "^9.28.0",
|
|
41
|
+
"@types/node": "^22.15.30",
|
|
42
|
+
"eslint": "^9.28.0",
|
|
43
|
+
"eslint-config-prettier": "^10.1.5",
|
|
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.9.1",
|
|
46
|
+
"eslint-plugin-unicorn": "^59.0.1",
|
|
47
|
+
"globals": "^16.2.0",
|
|
48
48
|
"prettier": "^3.5.3",
|
|
49
49
|
"prettier-plugin-jsdoc": "^1.3.2",
|
|
50
|
-
"prettier-plugin-svelte": "^3.
|
|
51
|
-
"svelte-check": "^4.1
|
|
50
|
+
"prettier-plugin-svelte": "^3.4.0",
|
|
51
|
+
"svelte-check": "^4.2.1",
|
|
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.33.1",
|
|
55
|
+
"vite": "^6.3.5",
|
|
56
|
+
"vitest": "^3.2.2"
|
|
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,8 @@ export let params = $state({ value: {} });
|
|
|
16
16
|
|
|
17
17
|
export let location = $state(updatedLocation());
|
|
18
18
|
|
|
19
|
+
let meta = $state({ value: {} });
|
|
20
|
+
|
|
19
21
|
let navigationIndex = 0;
|
|
20
22
|
let pendingNavigationIndex = 0;
|
|
21
23
|
|
|
@@ -44,10 +46,19 @@ export function createRouter(r) {
|
|
|
44
46
|
p: constructPath,
|
|
45
47
|
navigate,
|
|
46
48
|
isActive,
|
|
49
|
+
async preload(pathname) {
|
|
50
|
+
await preload(routes, pathname);
|
|
51
|
+
},
|
|
47
52
|
route: {
|
|
48
53
|
get params() {
|
|
49
54
|
return params.value;
|
|
50
55
|
},
|
|
56
|
+
getParams(pathname) {
|
|
57
|
+
if (!isActive(pathname)) {
|
|
58
|
+
throw new Error(`\`${pathname}\` does not match the current route`);
|
|
59
|
+
}
|
|
60
|
+
return params.value;
|
|
61
|
+
},
|
|
51
62
|
get pathname() {
|
|
52
63
|
return /** @type {import('./index.d.ts').Path<T>} */ (location.pathname);
|
|
53
64
|
},
|
|
@@ -60,6 +71,9 @@ export function createRouter(r) {
|
|
|
60
71
|
get hash() {
|
|
61
72
|
return location.hash;
|
|
62
73
|
},
|
|
74
|
+
get meta() {
|
|
75
|
+
return meta.value;
|
|
76
|
+
},
|
|
63
77
|
},
|
|
64
78
|
};
|
|
65
79
|
}
|
|
@@ -101,7 +115,7 @@ export async function onNavigate(path, options = {}) {
|
|
|
101
115
|
if (base.name && matchPath.startsWith(base.name)) {
|
|
102
116
|
matchPath = matchPath.slice(base.name.length) || '/';
|
|
103
117
|
}
|
|
104
|
-
const { match, layouts, hooks, params: newParams } = matchRoute(matchPath, routes);
|
|
118
|
+
const { match, layouts, hooks, meta: newMeta, params: newParams } = matchRoute(matchPath, routes);
|
|
105
119
|
|
|
106
120
|
for (const { beforeLoad } of hooks) {
|
|
107
121
|
try {
|
|
@@ -137,7 +151,8 @@ export async function onNavigate(path, options = {}) {
|
|
|
137
151
|
} else {
|
|
138
152
|
componentTree.value = routeComponents;
|
|
139
153
|
}
|
|
140
|
-
params.value = newParams
|
|
154
|
+
params.value = newParams;
|
|
155
|
+
meta.value = newMeta;
|
|
141
156
|
syncSearchParams();
|
|
142
157
|
Object.assign(location, updatedLocation());
|
|
143
158
|
|
|
@@ -14,6 +14,7 @@ const PARAM_FILENAME_REGEX = /(?<=[/.]|^)\(?\[([\w-]+)\]\)?(\.lazy)?\.svelte$/;
|
|
|
14
14
|
const CATCH_ALL_FILENAME_REGEX = /(?<=[/.]|^)\(?\[\.\.\.([\w-]+)\]\)?(\.lazy)?\.svelte$/; // [...any].svelte, [...any].lazy.svelte, ([...any]).svelte
|
|
15
15
|
const OUT_OF_LAYOUT_FILENAME_REGEX = /(?<=[/.]|^)\(\[\.?\.?\.?([\w-]+)\]\)(\.lazy)?\.svelte$/; // ([any]).svelte, ([...any]).lazy.svelte
|
|
16
16
|
const HOOKS_FILENAME_REGEX = /(?<=[/.]|^)(hooks)(\.svelte)?\.(js|ts)$/; // hooks.js, hooks.svelte.js, hooks.ts, hooks.svelte.ts
|
|
17
|
+
const META_FILENAME_REGEX = /(?<=[/.]|^)(meta)(\.svelte)?\.(js|ts)$/; // meta.js, meta.svelte.js, meta.ts, meta.svelte.ts
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* @param {string} routesPath
|
|
@@ -44,7 +45,11 @@ export function buildFileTree(routesPath) {
|
|
|
44
45
|
tree.push({ name: entry, tree: buildFileTree(path.join(routesPath, entry)) });
|
|
45
46
|
continue;
|
|
46
47
|
}
|
|
47
|
-
if (
|
|
48
|
+
if (
|
|
49
|
+
!entry.endsWith('.svelte') &&
|
|
50
|
+
!HOOKS_FILENAME_REGEX.test(entry) &&
|
|
51
|
+
!META_FILENAME_REGEX.test(entry)
|
|
52
|
+
) {
|
|
48
53
|
continue;
|
|
49
54
|
}
|
|
50
55
|
tree.push(entry);
|
|
@@ -67,6 +72,10 @@ export function createRouteMap(fileTree, prefix = '') {
|
|
|
67
72
|
result['hooks'] = prefix + entry;
|
|
68
73
|
continue;
|
|
69
74
|
}
|
|
75
|
+
if (META_FILENAME_REGEX.test(entry)) {
|
|
76
|
+
result['meta'] = prefix + entry;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
70
79
|
continue;
|
|
71
80
|
}
|
|
72
81
|
|
|
@@ -101,7 +110,8 @@ export function createRouteMap(fileTree, prefix = '') {
|
|
|
101
110
|
result['/' + filePathToRoute(entry.replace('.svelte', ''))] = prefix + entry;
|
|
102
111
|
} else {
|
|
103
112
|
const entryName = filePathToRoute(entry.name);
|
|
104
|
-
|
|
113
|
+
const paramFolder = entryName.replace(/^\[(.*)\]$/, ':$1');
|
|
114
|
+
result['/' + paramFolder] = createRouteMap(entry.tree, prefix + entryName + '/');
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
return result;
|
|
@@ -137,7 +147,11 @@ export function createRouterCode(routes, routesPath, { allLazy = false } = {}) {
|
|
|
137
147
|
for (const [key, value] of Object.entries(routes)) {
|
|
138
148
|
if (typeof value === 'object') {
|
|
139
149
|
result[key] = handleImports(value, routesPath);
|
|
140
|
-
} else if (
|
|
150
|
+
} else if (
|
|
151
|
+
key === 'hooks' ||
|
|
152
|
+
key === 'meta' ||
|
|
153
|
+
(!value.endsWith('.lazy.svelte') && !allLazy)
|
|
154
|
+
) {
|
|
141
155
|
const variableName = pathToCorrectCasing(value);
|
|
142
156
|
importsMap.set(variableName, routesPath + value);
|
|
143
157
|
result[key] = variableName;
|
|
@@ -163,7 +177,7 @@ export function createRouterCode(routes, routesPath, { allLazy = false } = {}) {
|
|
|
163
177
|
`import { createRouter } from 'sv-router';`,
|
|
164
178
|
...imports,
|
|
165
179
|
'',
|
|
166
|
-
`export const { p, navigate, isActive, route } = createRouter(${stringifiedRoutes});`,
|
|
180
|
+
`export const { p, navigate, isActive, preload, route } = createRouter(${stringifiedRoutes});`,
|
|
167
181
|
].join('\n');
|
|
168
182
|
}
|
|
169
183
|
|
|
@@ -189,6 +203,7 @@ export function pathToCorrectCasing(value) {
|
|
|
189
203
|
extractLastPart(CATCH_ALL_FILENAME_REGEX) ||
|
|
190
204
|
extractLastPart(PARAM_FILENAME_REGEX) ||
|
|
191
205
|
extractLastPart(HOOKS_FILENAME_REGEX) ||
|
|
206
|
+
extractLastPart(META_FILENAME_REGEX) ||
|
|
192
207
|
extractLastPart(FILENAME_REGEX);
|
|
193
208
|
if (!lastPart) {
|
|
194
209
|
throw new Error(`Invalid filename: ${value}`);
|
|
@@ -196,7 +211,8 @@ export function pathToCorrectCasing(value) {
|
|
|
196
211
|
parts.push(...lastPart.split('-'));
|
|
197
212
|
|
|
198
213
|
const uppercased = parts.map((part, index) => {
|
|
199
|
-
if (index === 0 && lastPart === 'hooks') return part;
|
|
214
|
+
if (index === 0 && (lastPart === 'hooks' || lastPart === 'meta')) return part;
|
|
215
|
+
part = part.replace(/^\[(.*)\]$/, '$1');
|
|
200
216
|
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
201
217
|
});
|
|
202
218
|
return uppercased.join('');
|
package/src/helpers/is-active.js
CHANGED
|
@@ -38,10 +38,12 @@ function compare(compareFn, pathname, params) {
|
|
|
38
38
|
const routeParts = location.pathname.split('/').slice(1);
|
|
39
39
|
for (const [index, pathPart] of pathParts.entries()) {
|
|
40
40
|
const routePart = routeParts[index];
|
|
41
|
-
if (
|
|
41
|
+
if (pathPart.startsWith(':')) {
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
if (pathPart !== routePart) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
45
47
|
}
|
|
46
|
-
return
|
|
48
|
+
return true;
|
|
47
49
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* @typedef {import('../index.d.ts').Hooks} Hooks
|
|
7
7
|
*
|
|
8
8
|
* @typedef {import('../index.d.ts').Routes} Routes
|
|
9
|
+
*
|
|
10
|
+
* @typedef {import('../index.d.ts').RouteMeta} RouteMeta
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -15,6 +17,7 @@
|
|
|
15
17
|
* match: RouteComponent | undefined;
|
|
16
18
|
* layouts: LayoutComponent[];
|
|
17
19
|
* hooks: Hooks[];
|
|
20
|
+
* meta: RouteMeta;
|
|
18
21
|
* params: Record<string, string>;
|
|
19
22
|
* breakFromLayouts: boolean;
|
|
20
23
|
* }}
|
|
@@ -39,6 +42,9 @@ export function matchRoute(pathname, routes) {
|
|
|
39
42
|
/** @type {Record<string, string>} */
|
|
40
43
|
let params = {};
|
|
41
44
|
|
|
45
|
+
/** @type {RouteMeta} */
|
|
46
|
+
let meta = {};
|
|
47
|
+
|
|
42
48
|
let breakFromLayouts = false;
|
|
43
49
|
|
|
44
50
|
outer: for (const route of allRoutes) {
|
|
@@ -69,7 +75,7 @@ export function matchRoute(pathname, routes) {
|
|
|
69
75
|
);
|
|
70
76
|
match = /** @type {RouteComponent} */ (routes[resolvedPath]);
|
|
71
77
|
break outer;
|
|
72
|
-
} else if (routePart !== pathPart) {
|
|
78
|
+
} else if (routePart !== pathPart?.toLowerCase()) {
|
|
73
79
|
break;
|
|
74
80
|
}
|
|
75
81
|
|
|
@@ -89,6 +95,10 @@ export function matchRoute(pathname, routes) {
|
|
|
89
95
|
hooks.push(routes.hooks);
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
if ('meta' in routes && routes.meta) {
|
|
99
|
+
meta = { ...meta, ...routes.meta };
|
|
100
|
+
}
|
|
101
|
+
|
|
92
102
|
if (typeof routeMatch === 'function') {
|
|
93
103
|
if (routeParts.length === pathParts.length) {
|
|
94
104
|
match = routeMatch;
|
|
@@ -103,6 +113,7 @@ export function matchRoute(pathname, routes) {
|
|
|
103
113
|
match = result.match;
|
|
104
114
|
params = { ...params, ...result.params };
|
|
105
115
|
hooks.push(...result.hooks);
|
|
116
|
+
meta = { ...meta, ...result.meta };
|
|
106
117
|
if (result.breakFromLayouts) {
|
|
107
118
|
layouts = [];
|
|
108
119
|
} else {
|
|
@@ -113,7 +124,7 @@ export function matchRoute(pathname, routes) {
|
|
|
113
124
|
}
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
return { match, layouts, hooks, params, breakFromLayouts };
|
|
127
|
+
return { match, layouts, hooks, params, meta, breakFromLayouts };
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
/**
|
|
@@ -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/helpers/utils.js
CHANGED
|
@@ -25,7 +25,7 @@ export function resolveRouteComponents(input) {
|
|
|
25
25
|
* @param {import('../index.d.ts').RouteComponent} input
|
|
26
26
|
* @returns {Promise<import('svelte').Component>}
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
function resolveRouteComponent(input) {
|
|
29
29
|
return new Promise((resolve) => {
|
|
30
30
|
if (isLazyImport(input)) {
|
|
31
31
|
Promise.resolve(input()).then((module) => {
|
|
@@ -26,9 +26,12 @@ export function validateRoutes(routes) {
|
|
|
26
26
|
export function getRoutePaths(routes) {
|
|
27
27
|
const paths = [];
|
|
28
28
|
for (const [key, value] of Object.entries(routes)) {
|
|
29
|
+
if (['layout', 'hooks', 'meta'].includes(key)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
29
32
|
if (typeof value === 'object') {
|
|
30
33
|
paths.push(
|
|
31
|
-
...getRoutePaths(value).map((path) => {
|
|
34
|
+
...getRoutePaths(/** @type {import('../index.d.ts').Routes} */ (value)).map((path) => {
|
|
32
35
|
if (path === '*') {
|
|
33
36
|
return key + '/*';
|
|
34
37
|
}
|
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 = {
|
|
@@ -69,6 +74,7 @@ export type Routes = {
|
|
|
69
74
|
[_: `*${string}` | `(*${string})`]: RouteComponent | undefined;
|
|
70
75
|
layout?: LayoutComponent;
|
|
71
76
|
hooks?: Hooks;
|
|
77
|
+
meta?: RouteMeta;
|
|
72
78
|
};
|
|
73
79
|
|
|
74
80
|
export type IsActiveLink = Action<
|
|
@@ -76,6 +82,20 @@ export type IsActiveLink = Action<
|
|
|
76
82
|
{ className?: string; startsWith?: boolean } | undefined
|
|
77
83
|
>;
|
|
78
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Route metadata that can be extended via module augmentation.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* declare module 'sv-router' {
|
|
90
|
+
* interface RouteMeta {
|
|
91
|
+
* public?: boolean;
|
|
92
|
+
* requiresAuth?: boolean;
|
|
93
|
+
* }
|
|
94
|
+
* }
|
|
95
|
+
*/
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
97
|
+
export interface RouteMeta {}
|
|
98
|
+
|
|
79
99
|
export type RouterApi<T extends Routes> = {
|
|
80
100
|
/**
|
|
81
101
|
* Construct a path while ensuring type safety.
|
|
@@ -90,8 +110,9 @@ export type RouterApi<T extends Routes> = {
|
|
|
90
110
|
* @param params The parameters to replace in the route.
|
|
91
111
|
*/
|
|
92
112
|
p<U extends Path<T>>(...args: ConstructPathArgs<U>): string;
|
|
113
|
+
|
|
93
114
|
/**
|
|
94
|
-
* Navigate
|
|
115
|
+
* Navigate programmatically to a route.
|
|
95
116
|
*
|
|
96
117
|
* ```js
|
|
97
118
|
* navigate('/users');
|
|
@@ -124,6 +145,14 @@ export type RouterApi<T extends Routes> = {
|
|
|
124
145
|
<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
|
|
125
146
|
startsWith<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
|
|
126
147
|
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Preloads the given route.
|
|
151
|
+
*
|
|
152
|
+
* @param path The route to preload.
|
|
153
|
+
*/
|
|
154
|
+
preload<U extends Path<T>>(path: U): Promise<void>;
|
|
155
|
+
|
|
127
156
|
route: {
|
|
128
157
|
/**
|
|
129
158
|
* An object containing the parameters of the current route.
|
|
@@ -133,19 +162,32 @@ export type RouterApi<T extends Routes> = {
|
|
|
133
162
|
* 'hello-world', commentId: '123' }`.
|
|
134
163
|
*/
|
|
135
164
|
params: AllParams<T>;
|
|
165
|
+
/**
|
|
166
|
+
* Extract parameters from the given pathname. Will throw if the pathname does not match the
|
|
167
|
+
* current route.
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* route.getParams('/posts/:slug').slug;
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* @param pathname
|
|
174
|
+
*/
|
|
175
|
+
getParams<U extends Path<T>>(pathname: U): Record<ExtractParams<U>, string>;
|
|
136
176
|
/** The reactive pathname of the URL. */
|
|
137
|
-
pathname: Path<T
|
|
177
|
+
pathname: (Path<T, true> & {}) | (string & {});
|
|
138
178
|
/** The reactive query string part of the URL. */
|
|
139
179
|
search: string;
|
|
140
180
|
/** The reactive history state that can be passed to the `navigate` function. */
|
|
141
181
|
state: unknown;
|
|
142
182
|
/** The reactive hash part of the URL. */
|
|
143
183
|
hash: string;
|
|
184
|
+
/** Arbitrary metadata associated with the route. */
|
|
185
|
+
meta: RouteMeta;
|
|
144
186
|
};
|
|
145
187
|
};
|
|
146
188
|
|
|
147
|
-
export type Path<T extends Routes> = RemoveParenthesis<
|
|
148
|
-
RemoveLastSlash<RecursiveKeys<StripNonRoutes<T
|
|
189
|
+
export type Path<T extends Routes, AnyParam extends boolean = false> = RemoveParenthesis<
|
|
190
|
+
RemoveLastSlash<RecursiveKeys<StripNonRoutes<T>, '', AnyParam>>
|
|
149
191
|
>;
|
|
150
192
|
|
|
151
193
|
export type ConstructPathArgs<T extends string> =
|
|
@@ -155,9 +197,13 @@ export type IsActiveArgs<T extends string> =
|
|
|
155
197
|
PathParams<T> extends never ? [T] : [T] | [T, PathParams<T>];
|
|
156
198
|
|
|
157
199
|
export type PathParams<T extends string> =
|
|
158
|
-
ExtractParams<T
|
|
200
|
+
ExtractParams<RemoveParenthesis<T>> extends never
|
|
201
|
+
? never
|
|
202
|
+
: Record<ExtractParams<RemoveParenthesis<T>>, string>;
|
|
159
203
|
|
|
160
|
-
export type AllParams<T extends Routes> = Partial<
|
|
204
|
+
export type AllParams<T extends Routes> = Partial<
|
|
205
|
+
Record<ExtractParams<RemoveParenthesis<RecursiveKeys<T>>>, string>
|
|
206
|
+
>;
|
|
161
207
|
|
|
162
208
|
export type NavigateOptions =
|
|
163
209
|
| {
|
|
@@ -192,17 +238,33 @@ type StripNonRoutes<T extends Routes> = {
|
|
|
192
238
|
? never
|
|
193
239
|
: K extends 'hooks'
|
|
194
240
|
? never
|
|
195
|
-
: K
|
|
241
|
+
: K extends 'meta'
|
|
242
|
+
? never
|
|
243
|
+
: K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
|
|
196
244
|
};
|
|
197
245
|
|
|
198
|
-
type RecursiveKeys<
|
|
246
|
+
type RecursiveKeys<
|
|
247
|
+
T extends Routes,
|
|
248
|
+
Prefix extends string = '',
|
|
249
|
+
AnyParam extends boolean = false,
|
|
250
|
+
> = {
|
|
199
251
|
[K in keyof T]: K extends string
|
|
200
252
|
? T[K] extends Routes
|
|
201
|
-
? RecursiveKeys<
|
|
202
|
-
|
|
253
|
+
? RecursiveKeys<
|
|
254
|
+
T[K],
|
|
255
|
+
`${Prefix}${AnyParam extends true ? ReplaceParamWithString<K> : K}`,
|
|
256
|
+
AnyParam
|
|
257
|
+
>
|
|
258
|
+
: `${Prefix}${AnyParam extends true ? ReplaceParamWithString<K> : K}`
|
|
203
259
|
: never;
|
|
204
260
|
}[keyof T];
|
|
205
261
|
|
|
262
|
+
type ReplaceParamWithString<T extends string> = T extends `/:${string}`
|
|
263
|
+
? `/${string}`
|
|
264
|
+
: T extends `/(:${string})`
|
|
265
|
+
? `/${string}`
|
|
266
|
+
: T;
|
|
267
|
+
|
|
206
268
|
type RemoveLastSlash<T extends string> = T extends '/' ? T : T extends `${infer R}/` ? R : T;
|
|
207
269
|
|
|
208
270
|
type RemoveParenthesis<T extends string> = T extends `${infer A}(${infer B})${infer C}`
|
|
@@ -211,14 +273,10 @@ type RemoveParenthesis<T extends string> = T extends `${infer A}(${infer B})${in
|
|
|
211
273
|
|
|
212
274
|
type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
|
|
213
275
|
? Param | ExtractParams<`/${Rest}`>
|
|
214
|
-
: T extends `${string}
|
|
276
|
+
: T extends `${string}:${infer Param}`
|
|
215
277
|
? Param
|
|
216
|
-
: T extends `${string}
|
|
217
|
-
? Param
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
? Param extends ''
|
|
222
|
-
? never
|
|
223
|
-
: Param
|
|
224
|
-
: never;
|
|
278
|
+
: T extends `${string}*${infer Param}`
|
|
279
|
+
? Param extends ''
|
|
280
|
+
? never
|
|
281
|
+
: Param
|
|
282
|
+
: never;
|