sv-router 0.0.6 → 0.0.7
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 -17
- package/src/Router.svelte +1 -1
- package/src/create-router.svelte.js +48 -20
- package/src/gen/generate-router-code.js +120 -58
- package/src/helpers/match-route.js +12 -1
- package/src/helpers/utils.js +1 -1
- package/src/index.d.ts +32 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Modern Svelte
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "Modern Svelte Routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
7
7
|
"router",
|
|
@@ -36,31 +36,34 @@
|
|
|
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
|
|
39
|
+
"@changesets/cli": "^2.28.1",
|
|
40
|
+
"@eslint/js": "^9.21.0",
|
|
41
|
+
"@types/node": "^22.13.8",
|
|
42
|
+
"eslint": "^9.21.0",
|
|
43
|
+
"eslint-config-prettier": "^10.0.2",
|
|
43
44
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
44
|
-
"eslint-plugin-svelte": "^
|
|
45
|
-
"eslint-plugin-unicorn": "^
|
|
46
|
-
"globals": "^
|
|
47
|
-
"prettier": "^3.
|
|
45
|
+
"eslint-plugin-svelte": "^3.0.2",
|
|
46
|
+
"eslint-plugin-unicorn": "^57.0.0",
|
|
47
|
+
"globals": "^16.0.0",
|
|
48
|
+
"prettier": "^3.5.2",
|
|
48
49
|
"prettier-plugin-jsdoc": "^1.3.2",
|
|
49
50
|
"prettier-plugin-svelte": "^3.3.3",
|
|
51
|
+
"svelte-check": "^4.1.4",
|
|
50
52
|
"type-testing": "^0.2.0",
|
|
51
|
-
"typescript": "^5.
|
|
52
|
-
"typescript-eslint": "^8.
|
|
53
|
-
"vite": "^6.0
|
|
54
|
-
"vitest": "^3.0.
|
|
53
|
+
"typescript": "^5.8.2",
|
|
54
|
+
"typescript-eslint": "^8.25.0",
|
|
55
|
+
"vite": "^6.2.0",
|
|
56
|
+
"vitest": "^3.0.7"
|
|
55
57
|
},
|
|
56
58
|
"peerDependencies": {
|
|
57
59
|
"svelte": "^5"
|
|
58
60
|
},
|
|
59
61
|
"scripts": {
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
+
"docs:dev": "pnpm --filter docs dev",
|
|
63
|
+
"docs:build": "pnpm --filter docs build",
|
|
64
|
+
"docs:preview": "pnpm --filter docs preview",
|
|
62
65
|
"test": "vitest",
|
|
63
|
-
"check": "
|
|
66
|
+
"check": "svelte-check && pnpm -r check",
|
|
64
67
|
"lint": "eslint .",
|
|
65
68
|
"lint:fix": "eslint . --fix",
|
|
66
69
|
"format": "prettier . --check",
|
package/src/Router.svelte
CHANGED
|
@@ -57,40 +57,65 @@ export function createRouter(r) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* @param {string} path
|
|
60
|
+
* @param {string | number} path
|
|
61
61
|
* @param {import('./index.d.ts').NavigateOptions & { params?: Record<string, string> }} options
|
|
62
62
|
*/
|
|
63
63
|
function navigate(path, options = {}) {
|
|
64
|
+
if (typeof path === 'number') {
|
|
65
|
+
globalThis.history.go(path);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
64
68
|
if (options.params) {
|
|
65
69
|
path = constructPath(path, options.params);
|
|
66
70
|
}
|
|
67
|
-
if (options.search) {
|
|
68
|
-
|
|
71
|
+
if (options.search && !options.search.startsWith('?')) {
|
|
72
|
+
options.search = '?' + options.search;
|
|
69
73
|
}
|
|
70
|
-
if (options.hash) {
|
|
71
|
-
|
|
74
|
+
if (options.hash && !options.hash.startsWith('#')) {
|
|
75
|
+
options.hash = '#' + options.hash;
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
globalThis.history[historyMethod](options.state || {}, '', path);
|
|
75
|
-
onNavigate();
|
|
77
|
+
onNavigate(path, options);
|
|
76
78
|
}
|
|
77
|
-
navigate.back = () => globalThis.history.back();
|
|
78
|
-
navigate.forward = () => globalThis.history.forward();
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} [path]
|
|
82
|
+
* @param {import('./index.d.ts').NavigateOptions} options
|
|
83
|
+
*/
|
|
84
|
+
export async function onNavigate(path, options = {}) {
|
|
81
85
|
if (!routes) {
|
|
82
86
|
throw new Error('Router not initialized: `createRouter` was not called.');
|
|
83
87
|
}
|
|
88
|
+
const {
|
|
89
|
+
match,
|
|
90
|
+
layouts,
|
|
91
|
+
hooks,
|
|
92
|
+
params: newParams,
|
|
93
|
+
} = matchRoute(path || globalThis.location.pathname, routes);
|
|
94
|
+
|
|
95
|
+
for (const { beforeLoad } of hooks) {
|
|
96
|
+
try {
|
|
97
|
+
await beforeLoad?.();
|
|
98
|
+
} catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
84
102
|
|
|
85
|
-
|
|
103
|
+
componentTree.value = await resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
104
|
+
params.value = newParams || {};
|
|
86
105
|
|
|
106
|
+
if (path) {
|
|
107
|
+
if (options.search) path += options.search;
|
|
108
|
+
if (options.hash) path += options.hash;
|
|
109
|
+
const historyMethod = options.replace ? 'replaceState' : 'pushState';
|
|
110
|
+
globalThis.history[historyMethod](options.state || {}, '', path);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
syncSearchParams();
|
|
87
114
|
Object.assign(location, updatedLocation());
|
|
88
115
|
|
|
89
|
-
const {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
componentTree.value = components;
|
|
93
|
-
});
|
|
116
|
+
for (const { afterLoad } of hooks) {
|
|
117
|
+
afterLoad?.();
|
|
118
|
+
}
|
|
94
119
|
}
|
|
95
120
|
|
|
96
121
|
/** @param {Event} event */
|
|
@@ -106,9 +131,12 @@ export function onGlobalClick(event) {
|
|
|
106
131
|
|
|
107
132
|
event.preventDefault();
|
|
108
133
|
const { replace, state } = anchor.dataset;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
onNavigate(url.pathname, {
|
|
135
|
+
replace: replace === '' || replace === 'true',
|
|
136
|
+
search: url.search,
|
|
137
|
+
state,
|
|
138
|
+
hash: url.hash,
|
|
139
|
+
});
|
|
112
140
|
}
|
|
113
141
|
|
|
114
142
|
function updatedLocation() {
|
|
@@ -9,7 +9,9 @@ import path from 'node:path';
|
|
|
9
9
|
* }} GeneratedRoutes
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const PARAM_FILENAME_REGEX = /\[(.*)\].svelte
|
|
12
|
+
const PARAM_FILENAME_REGEX = /\[(.*)\](\.lazy)?\.svelte$/; // [any].svelte, [any].lazy.svelte
|
|
13
|
+
const CATCH_ALL_FILENAME_REGEX = /\[\.\.\.(.*)\](\.lazy)?\.svelte$/; // [...any].svelte, [...any].lazy.svelte
|
|
14
|
+
const HOOKS_FILENAME_REGEX = /(hooks)(\.svelte)?\.(js|ts)$/; // hooks.js, hooks.svelte.js, hooks.ts, hooks.svelte.ts
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* @param {string} routesPath
|
|
@@ -35,34 +37,12 @@ export function buildFileTree(routesPath) {
|
|
|
35
37
|
tree.push({ name: entry, tree: buildFileTree(path.join(routesPath, entry)) });
|
|
36
38
|
continue;
|
|
37
39
|
}
|
|
38
|
-
if (!entry.endsWith('.svelte'))
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return tree;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @param {FileTree} tree
|
|
46
|
-
* @param {string} path
|
|
47
|
-
*/
|
|
48
|
-
function handleFlatFilename(tree, path) {
|
|
49
|
-
// Split the path by the first dot that is not preceded or followed by another dot
|
|
50
|
-
const splited = path.split(/(?<!\.)\.(?!\.)/);
|
|
51
|
-
if (splited.length === 2) {
|
|
52
|
-
tree.push(path);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
const first = /** @type {string} */ (splited.shift());
|
|
56
|
-
for (const item of tree) {
|
|
57
|
-
if (typeof item === 'object' && item.name === first) {
|
|
58
|
-
handleFlatFilename(item.tree, splited.join('.'));
|
|
59
|
-
return;
|
|
40
|
+
if (!entry.endsWith('.svelte') && !HOOKS_FILENAME_REGEX.test(entry)) {
|
|
41
|
+
continue;
|
|
60
42
|
}
|
|
43
|
+
tree.push(entry);
|
|
61
44
|
}
|
|
62
|
-
|
|
63
|
-
const branch = [];
|
|
64
|
-
handleFlatFilename(branch, splited.join('.'));
|
|
65
|
-
tree.push({ name: first, tree: branch });
|
|
45
|
+
return tree;
|
|
66
46
|
}
|
|
67
47
|
|
|
68
48
|
/**
|
|
@@ -75,36 +55,56 @@ export function createRouteMap(fileTree, prefix = '') {
|
|
|
75
55
|
const result = {};
|
|
76
56
|
for (const entry of fileTree) {
|
|
77
57
|
if (typeof entry === 'string') {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
case entry === 'layout.svelte': {
|
|
85
|
-
result['layout'] = prefix + entry;
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
case !!catchAll: {
|
|
89
|
-
result['*' + catchAll[1]] = prefix + entry;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
default: {
|
|
93
|
-
if (PARAM_FILENAME_REGEX.test(entry)) {
|
|
94
|
-
result['/' + entry.replaceAll(PARAM_FILENAME_REGEX, ':$1')] = prefix + entry;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
result['/' + entry.replace('.svelte', '')] = prefix + entry;
|
|
98
|
-
break;
|
|
58
|
+
if (!entry.endsWith('.svelte')) {
|
|
59
|
+
if (HOOKS_FILENAME_REGEX.test(entry)) {
|
|
60
|
+
result['hooks'] = prefix + entry;
|
|
61
|
+
continue;
|
|
99
62
|
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (entry.endsWith('index.svelte') || entry.endsWith('index.lazy.svelte')) {
|
|
67
|
+
const indexEntry = entry.replace(/\.?index(\.lazy)?\.svelte/, '');
|
|
68
|
+
result['/' + (indexEntry ? filePathToRoute(indexEntry) : '')] = prefix + entry;
|
|
69
|
+
continue;
|
|
100
70
|
}
|
|
71
|
+
|
|
72
|
+
if (entry === 'layout.svelte' || entry === 'layout.lazy.svelte') {
|
|
73
|
+
result['layout'] = prefix + entry;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const catchAll = CATCH_ALL_FILENAME_REGEX.exec(entry);
|
|
78
|
+
if (catchAll) {
|
|
79
|
+
result['*' + catchAll[1]] = prefix + entry;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Match [id].svelte
|
|
84
|
+
if (PARAM_FILENAME_REGEX.test(entry)) {
|
|
85
|
+
result['/' + filePathToRoute(entry.replace(PARAM_FILENAME_REGEX, ':$1'))] = prefix + entry;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
result['/' + filePathToRoute(entry.replace('.svelte', ''))] = prefix + entry;
|
|
101
90
|
} else {
|
|
102
|
-
|
|
91
|
+
const entryName = filePathToRoute(entry.name);
|
|
92
|
+
result['/' + entryName] = createRouteMap(entry.tree, prefix + entryName + '/');
|
|
103
93
|
}
|
|
104
94
|
}
|
|
105
95
|
return result;
|
|
106
96
|
}
|
|
107
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Replace `.` with `/`, but not `...`
|
|
100
|
+
*
|
|
101
|
+
* @param {string} filename
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
function filePathToRoute(filename) {
|
|
105
|
+
return filename.replaceAll(/\.(?!\.\.)/g, '/');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
108
|
/**
|
|
109
109
|
* @param {GeneratedRoutes} routes
|
|
110
110
|
* @param {string} routesPath
|
|
@@ -115,14 +115,76 @@ export function createRouterCode(routes, routesPath) {
|
|
|
115
115
|
routesPath += '/';
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
/** @type {Map<string, string>} */
|
|
119
|
+
const importsMap = new Map();
|
|
120
|
+
|
|
121
|
+
const withImports = (function handleImports(routes, routesPath) {
|
|
122
|
+
/** @type {GeneratedRoutes} */
|
|
123
|
+
const result = {};
|
|
124
|
+
for (const [key, value] of Object.entries(routes)) {
|
|
125
|
+
if (typeof value === 'object') {
|
|
126
|
+
result[key] = handleImports(value, routesPath);
|
|
127
|
+
} else if (key === 'hooks' || !value.endsWith('.lazy.svelte')) {
|
|
128
|
+
const variableName = pathToCorrectCasing(value);
|
|
129
|
+
importsMap.set(variableName, routesPath + value);
|
|
130
|
+
result[key] = variableName;
|
|
131
|
+
} else {
|
|
132
|
+
result[key] = `() => import('${routesPath}${value}')`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
})(routes, routesPath);
|
|
137
|
+
|
|
138
|
+
const imports = [...importsMap.entries()].map(([key, value]) => {
|
|
139
|
+
if (value.endsWith('.ts')) {
|
|
140
|
+
value = value.replace('.ts', '');
|
|
141
|
+
}
|
|
142
|
+
return `import ${key} from '${value}';`;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const stringifiedRoutes = JSON.stringify(withImports, undefined, 2)
|
|
146
|
+
.replaceAll(/"(.*)": /g, `'$1': `)
|
|
147
|
+
.replaceAll(/: "(.*)"/g, ': $1');
|
|
148
|
+
|
|
123
149
|
return [
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
150
|
+
`import { createRouter } from 'sv-router';`,
|
|
151
|
+
...imports,
|
|
152
|
+
'',
|
|
153
|
+
`export const { p, navigate, isActive, route } = createRouter(${stringifiedRoutes});`,
|
|
154
|
+
].join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {string} value
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
export function pathToCorrectCasing(value) {
|
|
162
|
+
const parts = /** @type {string[]} */ ([]);
|
|
163
|
+
|
|
164
|
+
/** @param {RegExp} regex */
|
|
165
|
+
function extractLastPart(regex) {
|
|
166
|
+
if (!regex.test(value)) return;
|
|
167
|
+
const exec = /** @type {RegExpExecArray} */ (regex.exec(value));
|
|
168
|
+
if (exec.index > 0) {
|
|
169
|
+
const before = value.slice(0, exec.index - 1);
|
|
170
|
+
parts.push(...before.split(/\/|-|\./));
|
|
171
|
+
}
|
|
172
|
+
return exec[1];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const lastPart =
|
|
176
|
+
extractLastPart(CATCH_ALL_FILENAME_REGEX) ||
|
|
177
|
+
extractLastPart(PARAM_FILENAME_REGEX) ||
|
|
178
|
+
extractLastPart(HOOKS_FILENAME_REGEX) ||
|
|
179
|
+
extractLastPart(/([\w-]+)(\.lazy)?\.svelte$/);
|
|
180
|
+
if (!lastPart) {
|
|
181
|
+
throw new Error(`Invalid filename: ${value}`);
|
|
182
|
+
}
|
|
183
|
+
parts.push(...lastPart.split('-'));
|
|
184
|
+
|
|
185
|
+
const uppercased = parts.map((part, index) => {
|
|
186
|
+
if (index === 0 && lastPart === 'hooks') return part;
|
|
187
|
+
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
188
|
+
});
|
|
189
|
+
return uppercased.join('');
|
|
128
190
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @typedef {import('../index.d.ts').RouteComponent} RouteComponent
|
|
5
5
|
*
|
|
6
|
+
* @typedef {import('../index.d.ts').Hooks} Hooks
|
|
7
|
+
*
|
|
6
8
|
* @typedef {import('../index.d.ts').Routes} Routes
|
|
7
9
|
*/
|
|
8
10
|
|
|
@@ -12,6 +14,7 @@
|
|
|
12
14
|
* @returns {{
|
|
13
15
|
* match: RouteComponent | undefined;
|
|
14
16
|
* layouts: LayoutComponent[];
|
|
17
|
+
* hooks: Hooks[];
|
|
15
18
|
* params: Record<string, string>;
|
|
16
19
|
* breakFromLayouts: boolean;
|
|
17
20
|
* }}
|
|
@@ -30,6 +33,9 @@ export function matchRoute(pathname, routes) {
|
|
|
30
33
|
/** @type {LayoutComponent[]} */
|
|
31
34
|
let layouts = [];
|
|
32
35
|
|
|
36
|
+
/** @type {Hooks[]} */
|
|
37
|
+
let hooks = [];
|
|
38
|
+
|
|
33
39
|
/** @type {Record<string, string>} */
|
|
34
40
|
let params = {};
|
|
35
41
|
|
|
@@ -74,6 +80,10 @@ export function matchRoute(pathname, routes) {
|
|
|
74
80
|
layouts.push(routes.layout);
|
|
75
81
|
}
|
|
76
82
|
|
|
83
|
+
if ('hooks' in routes && routes.hooks) {
|
|
84
|
+
hooks.push(routes.hooks);
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
if (typeof routeMatch === 'function') {
|
|
78
88
|
if (routeParts.length === pathParts.length) {
|
|
79
89
|
match = routeMatch;
|
|
@@ -87,6 +97,7 @@ export function matchRoute(pathname, routes) {
|
|
|
87
97
|
if (result) {
|
|
88
98
|
match = result.match;
|
|
89
99
|
params = { ...params, ...result.params };
|
|
100
|
+
hooks.push(...result.hooks);
|
|
90
101
|
if (result.breakFromLayouts) {
|
|
91
102
|
layouts = [];
|
|
92
103
|
} else {
|
|
@@ -97,7 +108,7 @@ export function matchRoute(pathname, routes) {
|
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
return { match, layouts, params, breakFromLayouts };
|
|
111
|
+
return { match, layouts, hooks, params, breakFromLayouts };
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
/**
|
package/src/helpers/utils.js
CHANGED
|
@@ -42,5 +42,5 @@ export function resolveRouteComponent(input) {
|
|
|
42
42
|
* @returns {input is import('../index.d.ts').LazyRouteComponent}
|
|
43
43
|
*/
|
|
44
44
|
export function isLazyImport(input) {
|
|
45
|
-
return typeof input === 'function' && !!/\(\)\s?=>\s?import\(.*\)
|
|
45
|
+
return typeof input === 'function' && !!/\(\)\s?=>\s?import\(.*\)/.test(String(input));
|
|
46
46
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export const isActiveLink: IsActiveLink;
|
|
|
15
15
|
* Setup a new router instance with the given routes.
|
|
16
16
|
*
|
|
17
17
|
* ```js
|
|
18
|
-
* export const { p, navigate, route } = createRouter({
|
|
18
|
+
* export const { p, navigate, isActive, route } = createRouter({
|
|
19
19
|
* '/': Home,
|
|
20
20
|
* '/about': About,
|
|
21
21
|
* ...
|
|
@@ -42,11 +42,30 @@ export type RouteComponent<Props extends BaseProps = any> =
|
|
|
42
42
|
| Component<Props>
|
|
43
43
|
| LazyRouteComponent<Props>;
|
|
44
44
|
export type LayoutComponent = RouteComponent<{ children: Snippet }>;
|
|
45
|
+
export type Hooks = {
|
|
46
|
+
/**
|
|
47
|
+
* A function that will be called before the route is loaded. If it returns a promise, the route
|
|
48
|
+
* will wait for it to resolve before loading.
|
|
49
|
+
*
|
|
50
|
+
* You can throw a `navigate` call to redirect to another route.
|
|
51
|
+
*
|
|
52
|
+
* ```js
|
|
53
|
+
* async beforeLoad() {
|
|
54
|
+
* await ...
|
|
55
|
+
* throw navigate('/home');
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
beforeLoad?(): void | Promise<void>;
|
|
60
|
+
/** A function that will be called after the route is loaded. */
|
|
61
|
+
afterLoad?(): void;
|
|
62
|
+
};
|
|
45
63
|
|
|
46
64
|
export type Routes = {
|
|
47
65
|
[_: `/${string}`]: RouteComponent | Routes;
|
|
48
66
|
[_: `*${string}`]: RouteComponent | undefined;
|
|
49
67
|
layout?: LayoutComponent;
|
|
68
|
+
hooks?: Hooks;
|
|
50
69
|
};
|
|
51
70
|
|
|
52
71
|
export type IsActiveLink = Action<HTMLAnchorElement, { className?: string } | undefined>;
|
|
@@ -77,18 +96,15 @@ export type RouterApi<T extends Routes> = {
|
|
|
77
96
|
* },
|
|
78
97
|
* });
|
|
79
98
|
* // Back and forward
|
|
80
|
-
* navigate
|
|
81
|
-
* navigate
|
|
99
|
+
* navigate(-1);
|
|
100
|
+
* navigate(2);
|
|
82
101
|
* ```
|
|
83
102
|
*
|
|
84
103
|
* @param route The route to navigate to.
|
|
85
104
|
* @param options The navigation options.
|
|
86
105
|
*/
|
|
87
|
-
navigate:
|
|
88
|
-
|
|
89
|
-
back: () => void;
|
|
90
|
-
forward: () => void;
|
|
91
|
-
};
|
|
106
|
+
navigate<U extends Path<T>>(...args: NavigateArgs<U>): void;
|
|
107
|
+
|
|
92
108
|
/**
|
|
93
109
|
* Will return `true` if the given path is active.
|
|
94
110
|
*
|
|
@@ -139,21 +155,24 @@ export type NavigateOptions =
|
|
|
139
155
|
replace?: boolean;
|
|
140
156
|
search?: string;
|
|
141
157
|
state?: string;
|
|
142
|
-
hash?:
|
|
158
|
+
hash?: string;
|
|
143
159
|
}
|
|
144
160
|
| undefined;
|
|
145
161
|
|
|
146
162
|
type NavigateArgs<T extends string> =
|
|
147
|
-
PathParams<T> extends never
|
|
148
|
-
|
|
149
|
-
|
|
163
|
+
| (PathParams<T> extends never
|
|
164
|
+
? [T] | [T, NavigateOptions]
|
|
165
|
+
: [T, NavigateOptions & { params: PathParams<T> }])
|
|
166
|
+
| [number];
|
|
150
167
|
|
|
151
168
|
type StripNonRoutes<T extends Routes> = {
|
|
152
169
|
[K in keyof T as K extends `*${string}`
|
|
153
170
|
? never
|
|
154
171
|
: K extends 'layout'
|
|
155
172
|
? never
|
|
156
|
-
: K
|
|
173
|
+
: K extends 'hooks'
|
|
174
|
+
? never
|
|
175
|
+
: K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
|
|
157
176
|
};
|
|
158
177
|
|
|
159
178
|
type RecursiveKeys<T extends Routes, Prefix extends string = ''> = {
|