sv-router 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/RecursiveComponentTree.svelte +2 -2
- package/src/Router.svelte +1 -1
- package/src/actions.svelte.js +17 -0
- package/src/cli/index.js +17 -3
- package/src/create-router.svelte.js +72 -18
- package/src/gen/config.js +5 -1
- package/src/gen/generate-router-code.js +38 -12
- package/src/helpers/is-active.js +31 -0
- package/src/helpers/match-route.js +33 -25
- package/src/helpers/preload-on-hover.js +28 -0
- package/src/index.d.ts +115 -11
- package/src/index.js +2 -0
- package/src/search-params.svelte.js +78 -0
- package/src/vite-plugin/index.d.ts +6 -0
- package/src/vite-plugin/plugin.js +2 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Modern Svelte routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -33,31 +33,31 @@
|
|
|
33
33
|
"src"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"esm-env": "^1.2.
|
|
36
|
+
"esm-env": "^1.2.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@changesets/cli": "^2.27.
|
|
40
|
-
"@eslint/js": "^9.
|
|
41
|
-
"@types/node": "^22.10.
|
|
42
|
-
"eslint-config-prettier": "^
|
|
39
|
+
"@changesets/cli": "^2.27.11",
|
|
40
|
+
"@eslint/js": "^9.18.0",
|
|
41
|
+
"@types/node": "^22.10.7",
|
|
42
|
+
"eslint-config-prettier": "^10.0.1",
|
|
43
43
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
44
44
|
"eslint-plugin-svelte": "^2.46.1",
|
|
45
45
|
"eslint-plugin-unicorn": "^56.0.1",
|
|
46
|
-
"globals": "^15.
|
|
46
|
+
"globals": "^15.14.0",
|
|
47
47
|
"prettier": "^3.4.2",
|
|
48
|
-
"prettier-plugin-jsdoc": "^1.3.
|
|
49
|
-
"prettier-plugin-svelte": "^3.3.
|
|
48
|
+
"prettier-plugin-jsdoc": "^1.3.2",
|
|
49
|
+
"prettier-plugin-svelte": "^3.3.3",
|
|
50
50
|
"type-testing": "^0.2.0",
|
|
51
|
-
"typescript": "^5.7.
|
|
52
|
-
"typescript-eslint": "^8.
|
|
53
|
-
"vite": "^6.0.
|
|
54
|
-
"vitest": "^
|
|
51
|
+
"typescript": "^5.7.3",
|
|
52
|
+
"typescript-eslint": "^8.20.0",
|
|
53
|
+
"vite": "^6.0.7",
|
|
54
|
+
"vitest": "^3.0.2"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"svelte": "^5"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
|
-
"ex:
|
|
60
|
+
"ex:config-based": "pnpm --filter config-based-example",
|
|
61
61
|
"ex:file-based": "pnpm --filter file-based-example",
|
|
62
62
|
"test": "vitest",
|
|
63
63
|
"check": "tsc --noEmit && pnpm -r check",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import {
|
|
2
|
+
import { params } from './create-router.svelte.js';
|
|
3
3
|
import RecursiveComponentTree from './RecursiveComponentTree.svelte';
|
|
4
4
|
|
|
5
5
|
/** @type {{ tree: import('svelte').Component[] }} */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
const restTree = $derived(tree.slice(1));
|
|
10
10
|
</script>
|
|
11
11
|
|
|
12
|
-
{#key restTree.length > 0 || Object.values(
|
|
12
|
+
{#key restTree.length > 0 || Object.values(params.value)}
|
|
13
13
|
<FirstComponent>
|
|
14
14
|
{#if restTree.length > 0}
|
|
15
15
|
<RecursiveComponentTree tree={restTree}></RecursiveComponentTree>
|
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
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -5,9 +5,23 @@ import { writeRouterCode } from '../gen/write-router-code.js';
|
|
|
5
5
|
|
|
6
6
|
const args = process.argv.slice(2).flatMap((arg) => arg.split('='));
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @param {keyof import('../vite-plugin/index.d.ts').RouterOptions} option
|
|
10
|
+
* @returns
|
|
11
|
+
*/
|
|
12
|
+
function arg(option) {
|
|
13
|
+
const pathArgIndex = args.indexOf('--' + option);
|
|
14
|
+
if (pathArgIndex === -1) return;
|
|
15
|
+
if (pathArgIndex + 1 < args.length) {
|
|
16
|
+
return args[pathArgIndex + 1];
|
|
17
|
+
}
|
|
18
|
+
return args[pathArgIndex];
|
|
11
19
|
}
|
|
12
20
|
|
|
21
|
+
const pathArg = arg('path');
|
|
22
|
+
if (pathArg) genConfig.routesPath = pathArg;
|
|
23
|
+
|
|
24
|
+
const jsArg = arg('js');
|
|
25
|
+
if (jsArg) genConfig.routesInJs = true;
|
|
26
|
+
|
|
13
27
|
writeRouterCode();
|
|
@@ -1,20 +1,25 @@
|
|
|
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';
|
|
4
|
+
import { preloadOnHover } from './helpers/preload-on-hover.js';
|
|
3
5
|
import { constructPath, resolveRouteComponents } from './helpers/utils.js';
|
|
6
|
+
import { syncSearchParams } from './search-params.svelte.js';
|
|
4
7
|
|
|
5
8
|
/** @type {import('./index.d.ts').Routes} */
|
|
6
9
|
export let routes;
|
|
7
10
|
|
|
8
|
-
/** @type {import('svelte').Component[]} */
|
|
9
|
-
export
|
|
11
|
+
/** @type {{ value: import('svelte').Component[] }} */
|
|
12
|
+
export let componentTree = $state({ value: [] });
|
|
10
13
|
|
|
11
|
-
/** @type {Record<string, string>} */
|
|
12
|
-
export
|
|
14
|
+
/** @type {{ value: Record<string, string> }} */
|
|
15
|
+
export let params = $state({ value: {} });
|
|
16
|
+
|
|
17
|
+
export let location = $state(updatedLocation());
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* @template {import('./index.d.ts').Routes} T
|
|
16
21
|
* @param {T} r
|
|
17
|
-
* @returns {import('./index.d.ts').
|
|
22
|
+
* @returns {import('./index.d.ts').RouterApi<T>}
|
|
18
23
|
*/
|
|
19
24
|
export function createRouter(r) {
|
|
20
25
|
routes = r;
|
|
@@ -25,29 +30,67 @@ export function createRouter(r) {
|
|
|
25
30
|
});
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
preloadOnHover(routes);
|
|
34
|
+
|
|
28
35
|
return {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
p: constructPath,
|
|
37
|
+
navigate,
|
|
38
|
+
isActive,
|
|
39
|
+
route: {
|
|
40
|
+
get params() {
|
|
41
|
+
return params.value;
|
|
42
|
+
},
|
|
43
|
+
get pathname() {
|
|
44
|
+
return location.pathname;
|
|
45
|
+
},
|
|
46
|
+
get search() {
|
|
47
|
+
return location.search;
|
|
48
|
+
},
|
|
49
|
+
get state() {
|
|
50
|
+
return location.state;
|
|
51
|
+
},
|
|
52
|
+
get hash() {
|
|
53
|
+
return location.hash;
|
|
54
|
+
},
|
|
38
55
|
},
|
|
39
56
|
};
|
|
40
57
|
}
|
|
41
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} path
|
|
61
|
+
* @param {import('./index.d.ts').NavigateOptions & { params?: Record<string, string> }} options
|
|
62
|
+
*/
|
|
63
|
+
function navigate(path, options = {}) {
|
|
64
|
+
if (options.params) {
|
|
65
|
+
path = constructPath(path, options.params);
|
|
66
|
+
}
|
|
67
|
+
if (options.search) {
|
|
68
|
+
path += (options.search.startsWith('?') ? '' : '?') + options.search;
|
|
69
|
+
}
|
|
70
|
+
if (options.hash) {
|
|
71
|
+
path += options.hash;
|
|
72
|
+
}
|
|
73
|
+
const historyMethod = options.replace ? 'replaceState' : 'pushState';
|
|
74
|
+
globalThis.history[historyMethod](options.state || {}, '', path);
|
|
75
|
+
onNavigate();
|
|
76
|
+
}
|
|
77
|
+
navigate.back = () => globalThis.history.back();
|
|
78
|
+
navigate.forward = () => globalThis.history.forward();
|
|
79
|
+
|
|
42
80
|
export function onNavigate() {
|
|
43
81
|
if (!routes) {
|
|
44
82
|
throw new Error('Router not initialized: `createRouter` was not called.');
|
|
45
83
|
}
|
|
46
|
-
|
|
84
|
+
|
|
85
|
+
syncSearchParams();
|
|
86
|
+
|
|
87
|
+
Object.assign(location, updatedLocation());
|
|
88
|
+
|
|
89
|
+
const { match, layouts, params: newParams } = matchRoute(globalThis.location.pathname, routes);
|
|
90
|
+
params.value = newParams || {};
|
|
47
91
|
resolveRouteComponents(match ? [...layouts, match] : layouts).then((components) => {
|
|
48
|
-
|
|
92
|
+
componentTree.value = components;
|
|
49
93
|
});
|
|
50
|
-
Object.assign(paramsStore, params);
|
|
51
94
|
}
|
|
52
95
|
|
|
53
96
|
/** @param {Event} event */
|
|
@@ -62,6 +105,17 @@ export function onGlobalClick(event) {
|
|
|
62
105
|
if (url.origin !== currentOrigin) return;
|
|
63
106
|
|
|
64
107
|
event.preventDefault();
|
|
65
|
-
|
|
108
|
+
const { replace, state } = anchor.dataset;
|
|
109
|
+
const historyMethod = replace === undefined || replace === 'false' ? 'pushState' : 'replaceState';
|
|
110
|
+
globalThis.history[historyMethod](state || {}, '', anchor.href);
|
|
66
111
|
onNavigate();
|
|
67
112
|
}
|
|
113
|
+
|
|
114
|
+
function updatedLocation() {
|
|
115
|
+
return {
|
|
116
|
+
pathname: globalThis.location.pathname,
|
|
117
|
+
search: globalThis.location.search,
|
|
118
|
+
state: globalThis.history.state,
|
|
119
|
+
hash: globalThis.location.hash,
|
|
120
|
+
};
|
|
121
|
+
}
|
package/src/gen/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @type {{
|
|
3
3
|
* routesPath: string;
|
|
4
|
+
* routesInJs: boolean;
|
|
4
5
|
* readonly genCodeDirPath: string;
|
|
5
6
|
* readonly routerPath: string;
|
|
6
7
|
* readonly tsconfigPath: string;
|
|
@@ -9,8 +10,11 @@
|
|
|
9
10
|
*/
|
|
10
11
|
export const genConfig = {
|
|
11
12
|
routesPath: 'src/routes',
|
|
13
|
+
routesInJs: false,
|
|
12
14
|
genCodeDirPath: '.router',
|
|
13
|
-
routerPath
|
|
15
|
+
get routerPath() {
|
|
16
|
+
return '.router/router.' + (this.routesInJs ? 'js' : 'ts');
|
|
17
|
+
},
|
|
14
18
|
tsconfigPath: '.router/tsconfig.json',
|
|
15
19
|
genCodeAlias: 'sv-router/generated',
|
|
16
20
|
};
|
|
@@ -28,16 +28,41 @@ export function generateRouterCode(routesPath) {
|
|
|
28
28
|
export function buildFileTree(routesPath) {
|
|
29
29
|
const entries = fs.readdirSync(routesPath);
|
|
30
30
|
/** @type {FileTree} */
|
|
31
|
-
const
|
|
31
|
+
const tree = [];
|
|
32
32
|
for (const entry of entries) {
|
|
33
33
|
const stat = fs.lstatSync(path.join(routesPath, entry));
|
|
34
34
|
if (stat.isDirectory()) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
result.push(entry);
|
|
35
|
+
tree.push({ name: entry, tree: buildFileTree(path.join(routesPath, entry)) });
|
|
36
|
+
continue;
|
|
38
37
|
}
|
|
38
|
+
if (!entry.endsWith('.svelte')) continue;
|
|
39
|
+
handleFlatFilename(tree, entry);
|
|
39
40
|
}
|
|
40
|
-
return
|
|
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;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** @type {FileTree} */
|
|
63
|
+
const branch = [];
|
|
64
|
+
handleFlatFilename(branch, splited.join('.'));
|
|
65
|
+
tree.push({ name: first, tree: branch });
|
|
41
66
|
}
|
|
42
67
|
|
|
43
68
|
/**
|
|
@@ -50,17 +75,18 @@ export function createRouteMap(fileTree, prefix = '') {
|
|
|
50
75
|
const result = {};
|
|
51
76
|
for (const entry of fileTree) {
|
|
52
77
|
if (typeof entry === 'string') {
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
const catchAll = /\[\.\.\.(.*)\]\.svelte/g.exec(entry); // Match [...slug].svelte
|
|
79
|
+
switch (true) {
|
|
80
|
+
case entry === 'index.svelte': {
|
|
55
81
|
result['/'] = prefix + entry;
|
|
56
82
|
break;
|
|
57
83
|
}
|
|
58
|
-
case '
|
|
59
|
-
result['
|
|
84
|
+
case entry === 'layout.svelte': {
|
|
85
|
+
result['layout'] = prefix + entry;
|
|
60
86
|
break;
|
|
61
87
|
}
|
|
62
|
-
case
|
|
63
|
-
result['
|
|
88
|
+
case !!catchAll: {
|
|
89
|
+
result['*' + catchAll[1]] = prefix + entry;
|
|
64
90
|
break;
|
|
65
91
|
}
|
|
66
92
|
default: {
|
|
@@ -97,6 +123,6 @@ export function createRouterCode(routes, routesPath) {
|
|
|
97
123
|
return [
|
|
98
124
|
'import { createRouter } from "sv-router";',
|
|
99
125
|
'\n\n',
|
|
100
|
-
`export const {
|
|
126
|
+
`export const { p, navigate, isActive, route } = createRouter(${withImports});`,
|
|
101
127
|
].join('');
|
|
102
128
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { location } from '../create-router.svelte.js';
|
|
2
|
+
import { constructPath } from './utils.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} pathname
|
|
6
|
+
* @param {Record<string, string>} [params]
|
|
7
|
+
* @returns {boolean}
|
|
8
|
+
*/
|
|
9
|
+
export function isActive(pathname, params) {
|
|
10
|
+
if (!pathname.includes(':')) {
|
|
11
|
+
return pathname === location.pathname;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (params) {
|
|
15
|
+
return constructPath(pathname, params) === location.pathname;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const pathParts = pathname.split('/').slice(1);
|
|
19
|
+
const routeParts = location.pathname.split('/').slice(1);
|
|
20
|
+
if (pathParts.length !== routeParts.length) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
for (const [index, pathPart] of pathParts.entries()) {
|
|
24
|
+
const routePart = routeParts[index];
|
|
25
|
+
if (routePart.startsWith(':')) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
return pathPart === routePart;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
@@ -21,8 +21,8 @@ export function matchRoute(pathname, routes) {
|
|
|
21
21
|
if (pathname.length > 1 && pathname.endsWith('/')) {
|
|
22
22
|
pathname = pathname.slice(0, -1);
|
|
23
23
|
}
|
|
24
|
-
const pathParts = pathname.split('/');
|
|
25
|
-
const
|
|
24
|
+
const pathParts = pathname.split('/').slice(1);
|
|
25
|
+
const allRoutes = sortRoutes(Object.keys(routes));
|
|
26
26
|
|
|
27
27
|
/** @type {RouteComponent | undefined} */
|
|
28
28
|
let match;
|
|
@@ -35,8 +35,11 @@ export function matchRoute(pathname, routes) {
|
|
|
35
35
|
|
|
36
36
|
let breakFromLayouts = false;
|
|
37
37
|
|
|
38
|
-
outer: for (const
|
|
39
|
-
|
|
38
|
+
outer: for (const route of allRoutes) {
|
|
39
|
+
const routeParts = route.split('/');
|
|
40
|
+
if (routeParts[0] === '') routeParts.shift();
|
|
41
|
+
|
|
42
|
+
for (let [index, routePart] of routeParts.entries()) {
|
|
40
43
|
breakFromLayouts = routePart.startsWith('(') && routePart.endsWith(')');
|
|
41
44
|
if (breakFromLayouts) {
|
|
42
45
|
routePart = routePart.slice(1, -1);
|
|
@@ -45,10 +48,15 @@ export function matchRoute(pathname, routes) {
|
|
|
45
48
|
const pathPart = pathParts[index];
|
|
46
49
|
if (routePart.startsWith(':')) {
|
|
47
50
|
params[routePart.slice(1)] = pathPart;
|
|
48
|
-
} else if (routePart
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
} else if (routePart.startsWith('*')) {
|
|
52
|
+
const param = routePart.slice(1);
|
|
53
|
+
if (param) {
|
|
54
|
+
params[param] = pathParts.slice(index).join('/');
|
|
55
|
+
}
|
|
56
|
+
const resolvedPath = /** @type {keyof Routes} */ (
|
|
57
|
+
(index ? '/' : '') + routeParts.join('/')
|
|
51
58
|
);
|
|
59
|
+
match = /** @type {RouteComponent} */ (routes[resolvedPath]);
|
|
52
60
|
break outer;
|
|
53
61
|
} else if (routePart !== pathPart) {
|
|
54
62
|
break;
|
|
@@ -58,31 +66,31 @@ export function matchRoute(pathname, routes) {
|
|
|
58
66
|
continue;
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
const routeMatch = /** @type {RouteComponent} */ (
|
|
70
|
+
routes[/** @type {keyof Routes} */ ('/' + routeParts.join('/'))]
|
|
71
|
+
);
|
|
72
|
+
|
|
61
73
|
if (!breakFromLayouts && 'layout' in routes && routes.layout) {
|
|
62
74
|
layouts.push(routes.layout);
|
|
63
75
|
}
|
|
64
76
|
|
|
65
|
-
const routeMatch = /** @type {RouteComponent} */ (
|
|
66
|
-
routes[/** @type {keyof Routes} */ (routeParts.join('/'))]
|
|
67
|
-
);
|
|
68
|
-
|
|
69
77
|
if (typeof routeMatch === 'function') {
|
|
70
78
|
if (routeParts.length === pathParts.length) {
|
|
71
79
|
match = routeMatch;
|
|
72
|
-
|
|
73
|
-
continue;
|
|
80
|
+
break outer;
|
|
74
81
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const nestedPathname = '/' + pathParts.slice(index + 1).join('/');
|
|
86
|
+
const result = matchRoute(nestedPathname, routeMatch);
|
|
87
|
+
if (result) {
|
|
88
|
+
match = result.match;
|
|
89
|
+
params = { ...params, ...result.params };
|
|
90
|
+
if (result.breakFromLayouts) {
|
|
91
|
+
layouts = [];
|
|
92
|
+
} else {
|
|
93
|
+
layouts.push(...result.layouts);
|
|
86
94
|
}
|
|
87
95
|
}
|
|
88
96
|
break outer;
|
|
@@ -106,7 +114,7 @@ export function sortRoutes(routes) {
|
|
|
106
114
|
*/
|
|
107
115
|
function getRoutePriority(route) {
|
|
108
116
|
if (route === '' || route === '/') return 1;
|
|
109
|
-
if (route
|
|
117
|
+
if (route.startsWith('*')) return 4;
|
|
110
118
|
if (route.includes(':')) return 3;
|
|
111
119
|
return 2;
|
|
112
120
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { matchRoute } from './match-route.js';
|
|
2
|
+
import { resolveRouteComponents } from './utils.js';
|
|
3
|
+
|
|
4
|
+
const linkSet = new Set();
|
|
5
|
+
|
|
6
|
+
/** @param {import('../index.d.ts').Routes} routes */
|
|
7
|
+
export function preloadOnHover(routes) {
|
|
8
|
+
const observer = new MutationObserver(() => {
|
|
9
|
+
const links = document.querySelectorAll('a[data-preload]');
|
|
10
|
+
for (const link of links) {
|
|
11
|
+
if (linkSet.has(link)) continue;
|
|
12
|
+
linkSet.add(link);
|
|
13
|
+
|
|
14
|
+
link.addEventListener('mouseenter', function callback() {
|
|
15
|
+
link.removeEventListener('mouseenter', callback);
|
|
16
|
+
const href = link.getAttribute('href');
|
|
17
|
+
if (!href) return;
|
|
18
|
+
const { match, layouts } = matchRoute(href, routes);
|
|
19
|
+
resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
observer.observe(document.body, {
|
|
25
|
+
subtree: true,
|
|
26
|
+
childList: true,
|
|
27
|
+
});
|
|
28
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
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 {
|
|
18
|
+
* export const { p, navigate, route } = createRouter({
|
|
8
19
|
* '/': Home,
|
|
9
20
|
* '/about': About,
|
|
10
21
|
* ...
|
|
11
22
|
* });
|
|
12
23
|
* ```
|
|
13
24
|
*/
|
|
14
|
-
export function createRouter<T extends Routes>(r: T):
|
|
25
|
+
export function createRouter<T extends Routes>(r: T): RouterApi<T>;
|
|
26
|
+
/** The component that will render the current route. */
|
|
15
27
|
export const Router: Component;
|
|
28
|
+
/**
|
|
29
|
+
* The reactive search params of the URL. It is just a wrapper around `SvelteURLSearchParam` that
|
|
30
|
+
* will update the url on change.
|
|
31
|
+
*/
|
|
32
|
+
export const searchParams: URLSearchParams;
|
|
16
33
|
|
|
17
34
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
18
35
|
type BaseProps = {};
|
|
@@ -28,14 +45,78 @@ export type LayoutComponent = RouteComponent<{ children: Snippet }>;
|
|
|
28
45
|
|
|
29
46
|
export type Routes = {
|
|
30
47
|
[_: `/${string}`]: RouteComponent | Routes;
|
|
31
|
-
|
|
48
|
+
[_: `*${string}`]: RouteComponent | undefined;
|
|
32
49
|
layout?: LayoutComponent;
|
|
33
50
|
};
|
|
34
51
|
|
|
35
|
-
export type
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
export type IsActiveLink = Action<HTMLAnchorElement, { className?: string } | undefined>;
|
|
53
|
+
|
|
54
|
+
export type RouterApi<T extends Routes> = {
|
|
55
|
+
/**
|
|
56
|
+
* Construct a path while ensuring type safety.
|
|
57
|
+
*
|
|
58
|
+
* ```js
|
|
59
|
+
* p('/users');
|
|
60
|
+
* // With parameters
|
|
61
|
+
* p('/users/:id', { id: 1 });
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @param route The route to navigate to.
|
|
65
|
+
* @param params The parameters to replace in the route.
|
|
66
|
+
*/
|
|
67
|
+
p<U extends Path<T>>(...args: ConstructPathArgs<U>): string;
|
|
68
|
+
/**
|
|
69
|
+
* Navigate programatically to a route.
|
|
70
|
+
*
|
|
71
|
+
* ```js
|
|
72
|
+
* navigate('/users');
|
|
73
|
+
* // With parameters
|
|
74
|
+
* navigate('/users/:id', {
|
|
75
|
+
* params: {
|
|
76
|
+
* id: 1,
|
|
77
|
+
* },
|
|
78
|
+
* });
|
|
79
|
+
* // Back and forward
|
|
80
|
+
* navigate.back();
|
|
81
|
+
* navigate.forward();
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @param route The route to navigate to.
|
|
85
|
+
* @param options The navigation options.
|
|
86
|
+
*/
|
|
87
|
+
navigate: {
|
|
88
|
+
<U extends Path<T>>(...args: NavigateArgs<U>): void;
|
|
89
|
+
back: () => void;
|
|
90
|
+
forward: () => void;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Will return `true` if the given path is active.
|
|
94
|
+
*
|
|
95
|
+
* Can be used with params to check the exact path, or without to check for any params in the
|
|
96
|
+
* path.
|
|
97
|
+
*
|
|
98
|
+
* @param path The route to check.
|
|
99
|
+
* @param params The optional parameters to replace in the route.
|
|
100
|
+
*/
|
|
101
|
+
isActive<U extends Path<T>>(...args: IsActiveArgs<U>): boolean;
|
|
102
|
+
route: {
|
|
103
|
+
/**
|
|
104
|
+
* An object containing the parameters of the current route.
|
|
105
|
+
*
|
|
106
|
+
* For example, given the route `/posts/:slug/comments/:commentId` and the URL
|
|
107
|
+
* `http://localhost:5173/posts/hello-world/comments/123`, the `params` object would be `{ slug:
|
|
108
|
+
* 'hello-world', commentId: '123' }`.
|
|
109
|
+
*/
|
|
110
|
+
params: AllParams<T>;
|
|
111
|
+
/** The reactive pathname of the URL. */
|
|
112
|
+
pathname: string;
|
|
113
|
+
/** The reactive query string part of the URL. */
|
|
114
|
+
search: string;
|
|
115
|
+
/** The reactive history state that can be passed to the `navigate` function. */
|
|
116
|
+
state: unknown;
|
|
117
|
+
/** The reactive hash part of the URL. */
|
|
118
|
+
hash: string;
|
|
119
|
+
};
|
|
39
120
|
};
|
|
40
121
|
|
|
41
122
|
export type Path<T extends Routes> = RemoveParenthesis<
|
|
@@ -45,15 +126,34 @@ export type Path<T extends Routes> = RemoveParenthesis<
|
|
|
45
126
|
export type ConstructPathArgs<T extends string> =
|
|
46
127
|
PathParams<T> extends never ? [T] : [T, PathParams<T>];
|
|
47
128
|
|
|
129
|
+
export type IsActiveArgs<T extends string> =
|
|
130
|
+
PathParams<T> extends never ? [T] : [T] | [T, PathParams<T>];
|
|
131
|
+
|
|
48
132
|
export type PathParams<T extends string> =
|
|
49
133
|
ExtractParams<T> extends never ? never : Record<ExtractParams<T>, string>;
|
|
50
134
|
|
|
51
135
|
export type AllParams<T extends Routes> = Partial<Record<ExtractParams<RecursiveKeys<T>>, string>>;
|
|
52
136
|
|
|
137
|
+
export type NavigateOptions =
|
|
138
|
+
| {
|
|
139
|
+
replace?: boolean;
|
|
140
|
+
search?: string;
|
|
141
|
+
state?: string;
|
|
142
|
+
hash?: `#${string}`;
|
|
143
|
+
}
|
|
144
|
+
| undefined;
|
|
145
|
+
|
|
146
|
+
type NavigateArgs<T extends string> =
|
|
147
|
+
PathParams<T> extends never
|
|
148
|
+
? [T, NavigateOptions]
|
|
149
|
+
: [T, NavigateOptions & { params: PathParams<T> }];
|
|
150
|
+
|
|
53
151
|
type StripNonRoutes<T extends Routes> = {
|
|
54
|
-
[K in keyof T as K extends
|
|
55
|
-
?
|
|
56
|
-
:
|
|
152
|
+
[K in keyof T as K extends `*${string}`
|
|
153
|
+
? never
|
|
154
|
+
: K extends 'layout'
|
|
155
|
+
? never
|
|
156
|
+
: K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
|
|
57
157
|
};
|
|
58
158
|
|
|
59
159
|
type RecursiveKeys<T extends Routes, Prefix extends string = ''> = {
|
|
@@ -74,4 +174,8 @@ type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${inf
|
|
|
74
174
|
? Param | ExtractParams<`/${Rest}`>
|
|
75
175
|
: T extends `${string}:${infer Param}`
|
|
76
176
|
? Param
|
|
77
|
-
:
|
|
177
|
+
: T extends `${string}*${infer Param}`
|
|
178
|
+
? Param extends ''
|
|
179
|
+
? never
|
|
180
|
+
: Param
|
|
181
|
+
: never;
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
|
2
|
+
|
|
3
|
+
let searchParams = new SvelteURLSearchParams(globalThis.location.search);
|
|
4
|
+
|
|
5
|
+
/** @type {URLSearchParams} */
|
|
6
|
+
const shell = {
|
|
7
|
+
append(...args) {
|
|
8
|
+
searchParams.append(...args);
|
|
9
|
+
updateUrlSearchParams();
|
|
10
|
+
},
|
|
11
|
+
delete(...args) {
|
|
12
|
+
searchParams.delete(...args);
|
|
13
|
+
updateUrlSearchParams();
|
|
14
|
+
},
|
|
15
|
+
entries() {
|
|
16
|
+
return searchParams.entries();
|
|
17
|
+
},
|
|
18
|
+
forEach(...args) {
|
|
19
|
+
// eslint-disable-next-line unicorn/no-array-for-each
|
|
20
|
+
return searchParams.forEach(...args);
|
|
21
|
+
},
|
|
22
|
+
get(...args) {
|
|
23
|
+
return searchParams.get(...args);
|
|
24
|
+
},
|
|
25
|
+
getAll(...args) {
|
|
26
|
+
return searchParams.getAll(...args);
|
|
27
|
+
},
|
|
28
|
+
has(...args) {
|
|
29
|
+
return searchParams.has(...args);
|
|
30
|
+
},
|
|
31
|
+
keys() {
|
|
32
|
+
return searchParams.keys();
|
|
33
|
+
},
|
|
34
|
+
set(...args) {
|
|
35
|
+
searchParams.set(...args);
|
|
36
|
+
updateUrlSearchParams();
|
|
37
|
+
},
|
|
38
|
+
sort() {
|
|
39
|
+
searchParams.sort();
|
|
40
|
+
updateUrlSearchParams();
|
|
41
|
+
},
|
|
42
|
+
toString() {
|
|
43
|
+
return searchParams.toString();
|
|
44
|
+
},
|
|
45
|
+
values() {
|
|
46
|
+
return searchParams.values();
|
|
47
|
+
},
|
|
48
|
+
get size() {
|
|
49
|
+
return searchParams.size;
|
|
50
|
+
},
|
|
51
|
+
[Symbol.iterator]() {
|
|
52
|
+
return searchParams[Symbol.iterator]();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export { shell as searchParams };
|
|
57
|
+
|
|
58
|
+
export function syncSearchParams() {
|
|
59
|
+
const newSearchParams = new URLSearchParams(globalThis.location.search);
|
|
60
|
+
if (searchParams.toString() === newSearchParams.toString()) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const key of searchParams.keys()) {
|
|
65
|
+
searchParams.delete(key);
|
|
66
|
+
}
|
|
67
|
+
for (const [key, value] of newSearchParams.entries()) {
|
|
68
|
+
searchParams.append(key, value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function updateUrlSearchParams() {
|
|
73
|
+
let url = globalThis.location.origin + globalThis.location.pathname;
|
|
74
|
+
if (searchParams.size > 0) {
|
|
75
|
+
url += '?' + searchParams.toString();
|
|
76
|
+
}
|
|
77
|
+
globalThis.history.pushState({}, '', url);
|
|
78
|
+
}
|
|
@@ -7,6 +7,12 @@ export type RouterOptions = {
|
|
|
7
7
|
* @default 'src/routes'
|
|
8
8
|
*/
|
|
9
9
|
path?: string;
|
|
10
|
+
/**
|
|
11
|
+
* If true, generates the routes in a .js file instead of a .ts file.
|
|
12
|
+
*
|
|
13
|
+
* @default false
|
|
14
|
+
*/
|
|
15
|
+
js?: boolean;
|
|
10
16
|
};
|
|
11
17
|
|
|
12
18
|
export const router: (options?: RouterOptions) => Plugin;
|
|
@@ -7,9 +7,8 @@ import { writeRouterCode } from '../gen/write-router-code.js';
|
|
|
7
7
|
* @returns {import('vite').Plugin}
|
|
8
8
|
*/
|
|
9
9
|
export function router(options) {
|
|
10
|
-
if (options?.path)
|
|
11
|
-
|
|
12
|
-
}
|
|
10
|
+
if (options?.path) genConfig.routesPath = options.path;
|
|
11
|
+
if (options?.js) genConfig.routesInJs = options.js;
|
|
13
12
|
|
|
14
13
|
return {
|
|
15
14
|
name: 'sv-router',
|