sv-router 0.0.5 → 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 +23 -20
- package/src/Router.svelte +1 -1
- package/src/actions.svelte.js +17 -0
- package/src/create-router.svelte.js +67 -28
- package/src/gen/generate-router-code.js +120 -56
- package/src/helpers/is-active.js +31 -0
- package/src/helpers/match-route.js +18 -3
- package/src/helpers/utils.js +1 -1
- package/src/index.d.ts +75 -11
- package/src/index.js +2 -0
- package/src/search-params.svelte.js +78 -0
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",
|
|
@@ -33,34 +33,37 @@
|
|
|
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.
|
|
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.
|
|
48
|
-
"prettier-plugin-jsdoc": "^1.3.
|
|
49
|
-
"prettier-plugin-svelte": "^3.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",
|
|
49
|
+
"prettier-plugin-jsdoc": "^1.3.2",
|
|
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": "^
|
|
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
|
@@ -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,16 +56,66 @@ export function createRouter(r) {
|
|
|
70
56
|
};
|
|
71
57
|
}
|
|
72
58
|
|
|
73
|
-
|
|
59
|
+
/**
|
|
60
|
+
* @param {string | number} path
|
|
61
|
+
* @param {import('./index.d.ts').NavigateOptions & { params?: Record<string, string> }} options
|
|
62
|
+
*/
|
|
63
|
+
function navigate(path, options = {}) {
|
|
64
|
+
if (typeof path === 'number') {
|
|
65
|
+
globalThis.history.go(path);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (options.params) {
|
|
69
|
+
path = constructPath(path, options.params);
|
|
70
|
+
}
|
|
71
|
+
if (options.search && !options.search.startsWith('?')) {
|
|
72
|
+
options.search = '?' + options.search;
|
|
73
|
+
}
|
|
74
|
+
if (options.hash && !options.hash.startsWith('#')) {
|
|
75
|
+
options.hash = '#' + options.hash;
|
|
76
|
+
}
|
|
77
|
+
onNavigate(path, options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} [path]
|
|
82
|
+
* @param {import('./index.d.ts').NavigateOptions} options
|
|
83
|
+
*/
|
|
84
|
+
export async function onNavigate(path, options = {}) {
|
|
74
85
|
if (!routes) {
|
|
75
86
|
throw new Error('Router not initialized: `createRouter` was not called.');
|
|
76
87
|
}
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
|
102
|
+
|
|
103
|
+
componentTree.value = await resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
79
104
|
params.value = newParams || {};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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();
|
|
114
|
+
Object.assign(location, updatedLocation());
|
|
115
|
+
|
|
116
|
+
for (const { afterLoad } of hooks) {
|
|
117
|
+
afterLoad?.();
|
|
118
|
+
}
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
/** @param {Event} event */
|
|
@@ -95,9 +131,12 @@ export function onGlobalClick(event) {
|
|
|
95
131
|
|
|
96
132
|
event.preventDefault();
|
|
97
133
|
const { replace, state } = anchor.dataset;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
134
|
+
onNavigate(url.pathname, {
|
|
135
|
+
replace: replace === '' || replace === 'true',
|
|
136
|
+
search: url.search,
|
|
137
|
+
state,
|
|
138
|
+
hash: url.hash,
|
|
139
|
+
});
|
|
101
140
|
}
|
|
102
141
|
|
|
103
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,33 +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
|
-
const splited = path.split('.');
|
|
50
|
-
if (splited.length === 2) {
|
|
51
|
-
tree.push(path);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const first = /** @type {string} */ (splited.shift());
|
|
55
|
-
for (const item of tree) {
|
|
56
|
-
if (typeof item === 'object' && item.name === first) {
|
|
57
|
-
handleFlatFilename(item.tree, splited.join('.'));
|
|
58
|
-
return;
|
|
40
|
+
if (!entry.endsWith('.svelte') && !HOOKS_FILENAME_REGEX.test(entry)) {
|
|
41
|
+
continue;
|
|
59
42
|
}
|
|
43
|
+
tree.push(entry);
|
|
60
44
|
}
|
|
61
|
-
|
|
62
|
-
const branch = [];
|
|
63
|
-
handleFlatFilename(branch, splited.join('.'));
|
|
64
|
-
tree.push({ name: first, tree: branch });
|
|
45
|
+
return tree;
|
|
65
46
|
}
|
|
66
47
|
|
|
67
48
|
/**
|
|
@@ -74,35 +55,56 @@ export function createRouteMap(fileTree, prefix = '') {
|
|
|
74
55
|
const result = {};
|
|
75
56
|
for (const entry of fileTree) {
|
|
76
57
|
if (typeof entry === 'string') {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
result['
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
case '*.svelte': {
|
|
83
|
-
result['*'] = prefix + entry;
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
case '_layout.svelte': {
|
|
87
|
-
result['layout'] = prefix + entry;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
default: {
|
|
91
|
-
if (PARAM_FILENAME_REGEX.test(entry)) {
|
|
92
|
-
result['/' + entry.replaceAll(PARAM_FILENAME_REGEX, ':$1')] = prefix + entry;
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
result['/' + entry.replace('.svelte', '')] = prefix + entry;
|
|
96
|
-
break;
|
|
58
|
+
if (!entry.endsWith('.svelte')) {
|
|
59
|
+
if (HOOKS_FILENAME_REGEX.test(entry)) {
|
|
60
|
+
result['hooks'] = prefix + entry;
|
|
61
|
+
continue;
|
|
97
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;
|
|
98
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;
|
|
99
90
|
} else {
|
|
100
|
-
|
|
91
|
+
const entryName = filePathToRoute(entry.name);
|
|
92
|
+
result['/' + entryName] = createRouteMap(entry.tree, prefix + entryName + '/');
|
|
101
93
|
}
|
|
102
94
|
}
|
|
103
95
|
return result;
|
|
104
96
|
}
|
|
105
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
|
+
|
|
106
108
|
/**
|
|
107
109
|
* @param {GeneratedRoutes} routes
|
|
108
110
|
* @param {string} routesPath
|
|
@@ -113,14 +115,76 @@ export function createRouterCode(routes, routesPath) {
|
|
|
113
115
|
routesPath += '/';
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
+
|
|
121
149
|
return [
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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('');
|
|
126
190
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
|
@@ -48,7 +54,11 @@ export function matchRoute(pathname, routes) {
|
|
|
48
54
|
const pathPart = pathParts[index];
|
|
49
55
|
if (routePart.startsWith(':')) {
|
|
50
56
|
params[routePart.slice(1)] = pathPart;
|
|
51
|
-
} else if (routePart
|
|
57
|
+
} else if (routePart.startsWith('*')) {
|
|
58
|
+
const param = routePart.slice(1);
|
|
59
|
+
if (param) {
|
|
60
|
+
params[param] = pathParts.slice(index).join('/');
|
|
61
|
+
}
|
|
52
62
|
const resolvedPath = /** @type {keyof Routes} */ (
|
|
53
63
|
(index ? '/' : '') + routeParts.join('/')
|
|
54
64
|
);
|
|
@@ -70,6 +80,10 @@ export function matchRoute(pathname, routes) {
|
|
|
70
80
|
layouts.push(routes.layout);
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
if ('hooks' in routes && routes.hooks) {
|
|
84
|
+
hooks.push(routes.hooks);
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
if (typeof routeMatch === 'function') {
|
|
74
88
|
if (routeParts.length === pathParts.length) {
|
|
75
89
|
match = routeMatch;
|
|
@@ -83,6 +97,7 @@ export function matchRoute(pathname, routes) {
|
|
|
83
97
|
if (result) {
|
|
84
98
|
match = result.match;
|
|
85
99
|
params = { ...params, ...result.params };
|
|
100
|
+
hooks.push(...result.hooks);
|
|
86
101
|
if (result.breakFromLayouts) {
|
|
87
102
|
layouts = [];
|
|
88
103
|
} else {
|
|
@@ -93,7 +108,7 @@ export function matchRoute(pathname, routes) {
|
|
|
93
108
|
}
|
|
94
109
|
}
|
|
95
110
|
|
|
96
|
-
return { match, layouts, params, breakFromLayouts };
|
|
111
|
+
return { match, layouts, hooks, params, breakFromLayouts };
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
/**
|
|
@@ -110,7 +125,7 @@ export function sortRoutes(routes) {
|
|
|
110
125
|
*/
|
|
111
126
|
function getRoutePriority(route) {
|
|
112
127
|
if (route === '' || route === '/') return 1;
|
|
113
|
-
if (route
|
|
128
|
+
if (route.startsWith('*')) return 4;
|
|
114
129
|
if (route.includes(':')) return 3;
|
|
115
130
|
return 2;
|
|
116
131
|
}
|
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
|
@@ -1,10 +1,21 @@
|
|
|
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
|
*
|
|
6
17
|
* ```js
|
|
7
|
-
* export const { p, navigate, route } = createRouter({
|
|
18
|
+
* export const { p, navigate, isActive, route } = createRouter({
|
|
8
19
|
* '/': Home,
|
|
9
20
|
* '/about': About,
|
|
10
21
|
* ...
|
|
@@ -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 = {};
|
|
@@ -25,13 +42,34 @@ export type RouteComponent<Props extends BaseProps = any> =
|
|
|
25
42
|
| Component<Props>
|
|
26
43
|
| LazyRouteComponent<Props>;
|
|
27
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
|
+
};
|
|
28
63
|
|
|
29
64
|
export type Routes = {
|
|
30
65
|
[_: `/${string}`]: RouteComponent | Routes;
|
|
31
|
-
|
|
66
|
+
[_: `*${string}`]: RouteComponent | undefined;
|
|
32
67
|
layout?: LayoutComponent;
|
|
68
|
+
hooks?: Hooks;
|
|
33
69
|
};
|
|
34
70
|
|
|
71
|
+
export type IsActiveLink = Action<HTMLAnchorElement, { className?: string } | undefined>;
|
|
72
|
+
|
|
35
73
|
export type RouterApi<T extends Routes> = {
|
|
36
74
|
/**
|
|
37
75
|
* Construct a path while ensuring type safety.
|
|
@@ -57,12 +95,26 @@ export type RouterApi<T extends Routes> = {
|
|
|
57
95
|
* id: 1,
|
|
58
96
|
* },
|
|
59
97
|
* });
|
|
98
|
+
* // Back and forward
|
|
99
|
+
* navigate(-1);
|
|
100
|
+
* navigate(2);
|
|
60
101
|
* ```
|
|
61
102
|
*
|
|
62
103
|
* @param route The route to navigate to.
|
|
63
104
|
* @param options The navigation options.
|
|
64
105
|
*/
|
|
65
106
|
navigate<U extends Path<T>>(...args: NavigateArgs<U>): void;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Will return `true` if the given path is active.
|
|
110
|
+
*
|
|
111
|
+
* Can be used with params to check the exact path, or without to check for any params in the
|
|
112
|
+
* path.
|
|
113
|
+
*
|
|
114
|
+
* @param path The route to check.
|
|
115
|
+
* @param params The optional parameters to replace in the route.
|
|
116
|
+
*/
|
|
117
|
+
isActive<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
|
|
66
118
|
route: {
|
|
67
119
|
/**
|
|
68
120
|
* An object containing the parameters of the current route.
|
|
@@ -90,6 +142,9 @@ export type Path<T extends Routes> = RemoveParenthesis<
|
|
|
90
142
|
export type ConstructPathArgs<T extends string> =
|
|
91
143
|
PathParams<T> extends never ? [T] : [T, PathParams<T>];
|
|
92
144
|
|
|
145
|
+
export type IsActiveArgs<T extends string> =
|
|
146
|
+
PathParams<T> extends never ? [T] : [T] | [T, PathParams<T>];
|
|
147
|
+
|
|
93
148
|
export type PathParams<T extends string> =
|
|
94
149
|
ExtractParams<T> extends never ? never : Record<ExtractParams<T>, string>;
|
|
95
150
|
|
|
@@ -100,19 +155,24 @@ export type NavigateOptions =
|
|
|
100
155
|
replace?: boolean;
|
|
101
156
|
search?: string;
|
|
102
157
|
state?: string;
|
|
103
|
-
hash?:
|
|
158
|
+
hash?: string;
|
|
104
159
|
}
|
|
105
160
|
| undefined;
|
|
106
161
|
|
|
107
|
-
|
|
108
|
-
PathParams<T> extends never
|
|
109
|
-
|
|
110
|
-
|
|
162
|
+
type NavigateArgs<T extends string> =
|
|
163
|
+
| (PathParams<T> extends never
|
|
164
|
+
? [T] | [T, NavigateOptions]
|
|
165
|
+
: [T, NavigateOptions & { params: PathParams<T> }])
|
|
166
|
+
| [number];
|
|
111
167
|
|
|
112
168
|
type StripNonRoutes<T extends Routes> = {
|
|
113
|
-
[K in keyof T as K extends
|
|
114
|
-
?
|
|
115
|
-
:
|
|
169
|
+
[K in keyof T as K extends `*${string}`
|
|
170
|
+
? never
|
|
171
|
+
: K extends 'layout'
|
|
172
|
+
? never
|
|
173
|
+
: K extends 'hooks'
|
|
174
|
+
? never
|
|
175
|
+
: K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
|
|
116
176
|
};
|
|
117
177
|
|
|
118
178
|
type RecursiveKeys<T extends Routes, Prefix extends string = ''> = {
|
|
@@ -133,4 +193,8 @@ type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${inf
|
|
|
133
193
|
? Param | ExtractParams<`/${Rest}`>
|
|
134
194
|
: T extends `${string}:${infer Param}`
|
|
135
195
|
? Param
|
|
136
|
-
:
|
|
196
|
+
: T extends `${string}*${infer Param}`
|
|
197
|
+
? Param extends ''
|
|
198
|
+
? never
|
|
199
|
+
: Param
|
|
200
|
+
: 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
|
+
}
|