sv-router 0.0.8 â 0.2.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 +6 -3
- package/package.json +13 -13
- package/src/Router.svelte +14 -1
- package/src/create-router.svelte.js +21 -11
- package/src/gen/generate-router-code.js +5 -5
- package/src/gen/write-router-code.js +10 -5
- package/src/helpers/utils.js +15 -0
- package/src/index.d.ts +14 -3
- package/src/search-params.svelte.js +16 -18
package/README.md
CHANGED
|
@@ -18,11 +18,14 @@ A feature-rich yet intuitive routing library for Svelte single-page apps.
|
|
|
18
18
|
## Features
|
|
19
19
|
|
|
20
20
|
- ð **Typesafe navigation**: Get autocomplete and type checking for your routes.
|
|
21
|
-
-
|
|
21
|
+
- ð **Flexibility**: Choose between code-based or file-based routing approaches.
|
|
22
22
|
- ðŋ **Nested routes**: Create complex layouts with ease.
|
|
23
|
+
- ð **Reactive search params**: For simpler state management in the URL.
|
|
24
|
+
- ðĄïļ **Hooks**: For navigation guards, data loading, or analytics tracking.
|
|
23
25
|
- ⥠**Performance**: Optimized for speed with built-in code splitting and preloading.
|
|
24
|
-
- ð§Đ **Familiar API**: Follows established conventions from popular meta frameworks
|
|
25
|
-
-
|
|
26
|
+
- ð§Đ **Familiar API**: Follows established conventions from popular meta frameworks.
|
|
27
|
+
- ðŠķ **Lightweight**: Minimal impact on your bundle size.
|
|
28
|
+
- ð **Made for Svelte 5**: True Svelte 5 implementation with the latest features.
|
|
26
29
|
|
|
27
30
|
## Installation
|
|
28
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Modern Svelte Routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -37,23 +37,23 @@
|
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@changesets/cli": "^2.28.1",
|
|
40
|
-
"@eslint/js": "^9.
|
|
41
|
-
"@types/node": "^22.13.
|
|
42
|
-
"eslint": "^9.
|
|
43
|
-
"eslint-config-prettier": "^10.
|
|
40
|
+
"@eslint/js": "^9.23.0",
|
|
41
|
+
"@types/node": "^22.13.14",
|
|
42
|
+
"eslint": "^9.23.0",
|
|
43
|
+
"eslint-config-prettier": "^10.1.1",
|
|
44
44
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
45
|
-
"eslint-plugin-svelte": "^3.
|
|
46
|
-
"eslint-plugin-unicorn": "^
|
|
45
|
+
"eslint-plugin-svelte": "^3.3.3",
|
|
46
|
+
"eslint-plugin-unicorn": "^58.0.0",
|
|
47
47
|
"globals": "^16.0.0",
|
|
48
|
-
"prettier": "^3.5.
|
|
48
|
+
"prettier": "^3.5.3",
|
|
49
49
|
"prettier-plugin-jsdoc": "^1.3.2",
|
|
50
50
|
"prettier-plugin-svelte": "^3.3.3",
|
|
51
|
-
"svelte-check": "^4.1.
|
|
51
|
+
"svelte-check": "^4.1.5",
|
|
52
52
|
"type-testing": "^0.2.0",
|
|
53
53
|
"typescript": "^5.8.2",
|
|
54
|
-
"typescript-eslint": "^8.
|
|
55
|
-
"vite": "^6.2.
|
|
56
|
-
"vitest": "^3.0.
|
|
54
|
+
"typescript-eslint": "^8.28.0",
|
|
55
|
+
"vite": "^6.2.3",
|
|
56
|
+
"vitest": "^3.0.9"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"svelte": "^5"
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"docs:preview": "pnpm --filter docs preview",
|
|
65
65
|
"test": "vitest",
|
|
66
66
|
"check": "svelte-check && pnpm -r check",
|
|
67
|
-
"lint": "eslint .",
|
|
67
|
+
"lint": "eslint . --max-warnings 0",
|
|
68
68
|
"lint:fix": "eslint . --fix",
|
|
69
69
|
"format": "prettier . --check",
|
|
70
70
|
"format:fix": "prettier . --write",
|
package/src/Router.svelte
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { on } from 'svelte/events';
|
|
3
|
-
import { componentTree, onGlobalClick, onNavigate } from './create-router.svelte.js';
|
|
3
|
+
import { base, componentTree, onGlobalClick, onNavigate } from './create-router.svelte.js';
|
|
4
|
+
import { join } from './helpers/utils.js';
|
|
4
5
|
import RecursiveComponentTree from './RecursiveComponentTree.svelte';
|
|
5
6
|
|
|
7
|
+
/** @type {{ base?: string }} */
|
|
8
|
+
let { base: basename } = $props();
|
|
9
|
+
|
|
10
|
+
if (basename) {
|
|
11
|
+
base.name = (basename.startsWith('/') ? '' : '/') + basename;
|
|
12
|
+
const url = new URL(globalThis.location.href);
|
|
13
|
+
if (!url.pathname.startsWith(base.name)) {
|
|
14
|
+
url.pathname = join(base.name, url.pathname);
|
|
15
|
+
history.replaceState(history.state || {}, '', url.href);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
onNavigate();
|
|
7
20
|
|
|
8
21
|
$effect(() => {
|
|
@@ -2,11 +2,11 @@ import { BROWSER, DEV } from 'esm-env';
|
|
|
2
2
|
import { isActive } from './helpers/is-active.js';
|
|
3
3
|
import { matchRoute } from './helpers/match-route.js';
|
|
4
4
|
import { preloadOnHover } from './helpers/preload-on-hover.js';
|
|
5
|
-
import { constructPath, resolveRouteComponents } from './helpers/utils.js';
|
|
5
|
+
import { constructPath, join, resolveRouteComponents } from './helpers/utils.js';
|
|
6
6
|
import { syncSearchParams } from './search-params.svelte.js';
|
|
7
7
|
|
|
8
8
|
/** @type {import('./index.d.ts').Routes} */
|
|
9
|
-
|
|
9
|
+
let routes;
|
|
10
10
|
|
|
11
11
|
/** @type {{ value: import('svelte').Component[] }} */
|
|
12
12
|
export let componentTree = $state({ value: [] });
|
|
@@ -16,6 +16,11 @@ export let params = $state({ value: {} });
|
|
|
16
16
|
|
|
17
17
|
export let location = $state(updatedLocation());
|
|
18
18
|
|
|
19
|
+
/** @type {{ name?: string }} */
|
|
20
|
+
export const base = {
|
|
21
|
+
name: undefined,
|
|
22
|
+
};
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* @template {import('./index.d.ts').Routes} T
|
|
21
26
|
* @param {T} r
|
|
@@ -85,12 +90,11 @@ export async function onNavigate(path, options = {}) {
|
|
|
85
90
|
if (!routes) {
|
|
86
91
|
throw new Error('Router not initialized: `createRouter` was not called.');
|
|
87
92
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
} = matchRoute(path || globalThis.location.pathname, routes);
|
|
93
|
+
let matchPath = path || globalThis.location.pathname;
|
|
94
|
+
if (base.name && matchPath.startsWith(base.name)) {
|
|
95
|
+
matchPath = matchPath.slice(base.name.length) || '/';
|
|
96
|
+
}
|
|
97
|
+
const { match, layouts, hooks, params: newParams } = matchRoute(matchPath, routes);
|
|
94
98
|
|
|
95
99
|
for (const { beforeLoad } of hooks) {
|
|
96
100
|
try {
|
|
@@ -107,12 +111,17 @@ export async function onNavigate(path, options = {}) {
|
|
|
107
111
|
if (options.search) path += options.search;
|
|
108
112
|
if (options.hash) path += options.hash;
|
|
109
113
|
const historyMethod = options.replace ? 'replaceState' : 'pushState';
|
|
110
|
-
|
|
114
|
+
const to = base.name ? join(base.name, path) : path;
|
|
115
|
+
globalThis.history[historyMethod](options.state || {}, '', to);
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
syncSearchParams();
|
|
114
119
|
Object.assign(location, updatedLocation());
|
|
115
120
|
|
|
121
|
+
if (options.scrollToTop !== false) {
|
|
122
|
+
window.scrollTo({ top: 0, left: 0, behavior: options.scrollToTop });
|
|
123
|
+
}
|
|
124
|
+
|
|
116
125
|
for (const { afterLoad } of hooks) {
|
|
117
126
|
afterLoad?.();
|
|
118
127
|
}
|
|
@@ -130,12 +139,13 @@ export function onGlobalClick(event) {
|
|
|
130
139
|
if (url.origin !== currentOrigin) return;
|
|
131
140
|
|
|
132
141
|
event.preventDefault();
|
|
133
|
-
const { replace, state } = anchor.dataset;
|
|
142
|
+
const { replace, state, scrollToTop } = anchor.dataset;
|
|
134
143
|
onNavigate(url.pathname, {
|
|
135
144
|
replace: replace === '' || replace === 'true',
|
|
136
145
|
search: url.search,
|
|
137
146
|
state,
|
|
138
147
|
hash: url.hash,
|
|
148
|
+
scrollToTop: scrollToTop === 'false' ? false : /** @type ScrollBehavior */ (scrollToTop),
|
|
139
149
|
});
|
|
140
150
|
}
|
|
141
151
|
|
|
@@ -143,7 +153,7 @@ function updatedLocation() {
|
|
|
143
153
|
return {
|
|
144
154
|
pathname: globalThis.location.pathname,
|
|
145
155
|
search: globalThis.location.search,
|
|
146
|
-
state:
|
|
156
|
+
state: history.state,
|
|
147
157
|
hash: globalThis.location.hash,
|
|
148
158
|
};
|
|
149
159
|
}
|
|
@@ -9,11 +9,11 @@ import path from 'node:path';
|
|
|
9
9
|
* }} GeneratedRoutes
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
const FILENAME_REGEX =
|
|
13
|
-
const PARAM_FILENAME_REGEX =
|
|
14
|
-
const CATCH_ALL_FILENAME_REGEX =
|
|
15
|
-
const OUT_OF_LAYOUT_FILENAME_REGEX =
|
|
16
|
-
const HOOKS_FILENAME_REGEX = /(hooks)(\.svelte)?\.(js|ts)$/; // hooks.js, hooks.svelte.js, hooks.ts, hooks.svelte.ts
|
|
12
|
+
const FILENAME_REGEX = /(?<=[/.]|^)\(?([\w-]+)\)?(\.lazy)?\.svelte$/; // any.svelte, any.lazy.svelte, (any).svelte
|
|
13
|
+
const PARAM_FILENAME_REGEX = /(?<=[/.]|^)\(?\[([\w-]+)\]\)?(\.lazy)?\.svelte$/; // [any].svelte, [any].lazy.svelte, ([any]).svelte
|
|
14
|
+
const CATCH_ALL_FILENAME_REGEX = /(?<=[/.]|^)\(?\[\.\.\.([\w-]+)\]\)?(\.lazy)?\.svelte$/; // [...any].svelte, [...any].lazy.svelte, ([...any]).svelte
|
|
15
|
+
const OUT_OF_LAYOUT_FILENAME_REGEX = /(?<=[/.]|^)\(\[\.?\.?\.?([\w-]+)\]\)(\.lazy)?\.svelte$/; // ([any]).svelte, ([...any]).lazy.svelte
|
|
16
|
+
const HOOKS_FILENAME_REGEX = /(?<=[/.]|^)(hooks)(\.svelte)?\.(js|ts)$/; // hooks.js, hooks.svelte.js, hooks.ts, hooks.svelte.ts
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* @param {string} routesPath
|
|
@@ -9,10 +9,6 @@ export function writeRouterCode() {
|
|
|
9
9
|
fs.mkdirSync(genConfig.genCodeDirPath);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// Write `.router/router.ts` file
|
|
13
|
-
const routerCode = generateRouterCode(genConfig.routesPath);
|
|
14
|
-
writeFileIfDifferent(genConfig.routerPath, routerCode);
|
|
15
|
-
|
|
16
12
|
// Write `.router/tsconfig.json` file
|
|
17
13
|
const tsConfig = {
|
|
18
14
|
compilerOptions: {
|
|
@@ -35,7 +31,15 @@ export function writeRouterCode() {
|
|
|
35
31
|
};
|
|
36
32
|
writeFileIfDifferent(genConfig.tsconfigPath, JSON.stringify(tsConfig, undefined, 2));
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
// Write `.router/router.ts` file
|
|
35
|
+
const routerCode = generateRouterCode(genConfig.routesPath);
|
|
36
|
+
const written = writeFileIfDifferent(genConfig.routerPath, routerCode);
|
|
37
|
+
|
|
38
|
+
if (written) {
|
|
39
|
+
console.log('â
ïļ Routes generated');
|
|
40
|
+
} else {
|
|
41
|
+
console.log('â
ïļ Routes already up to date');
|
|
42
|
+
}
|
|
39
43
|
} catch (error) {
|
|
40
44
|
console.error(
|
|
41
45
|
'Error during routes generation:',
|
|
@@ -51,5 +55,6 @@ export function writeRouterCode() {
|
|
|
51
55
|
function writeFileIfDifferent(filePath, content) {
|
|
52
56
|
if (!fs.existsSync(filePath) || fs.readFileSync(filePath, 'utf8') !== content) {
|
|
53
57
|
fs.writeFileSync(filePath, content);
|
|
58
|
+
return true;
|
|
54
59
|
}
|
|
55
60
|
}
|
package/src/helpers/utils.js
CHANGED
|
@@ -44,3 +44,18 @@ export function resolveRouteComponent(input) {
|
|
|
44
44
|
export function isLazyImport(input) {
|
|
45
45
|
return typeof input === 'function' && !!/\(\)\s?=>\s?import\(.*\)/.test(String(input));
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/** @param {...string} parts */
|
|
49
|
+
export function join(...parts) {
|
|
50
|
+
let result = '';
|
|
51
|
+
for (let part of parts) {
|
|
52
|
+
if (!part.startsWith('/')) {
|
|
53
|
+
result += '/';
|
|
54
|
+
}
|
|
55
|
+
if (part.endsWith('/')) {
|
|
56
|
+
part = part.slice(0, -1);
|
|
57
|
+
}
|
|
58
|
+
result += part;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -23,13 +23,16 @@ export const isActiveLink: IsActiveLink;
|
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
25
|
export function createRouter<T extends Routes>(r: T): RouterApi<T>;
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The component that will render the current route. You can pass a `base` prop to set the base path
|
|
28
|
+
* that is prepended to every url.
|
|
29
|
+
*/
|
|
30
|
+
export const Router: Component<{ base?: string }>;
|
|
28
31
|
/**
|
|
29
32
|
* The reactive search params of the URL. It is just a wrapper around `SvelteURLSearchParam` that
|
|
30
33
|
* will update the url on change.
|
|
31
34
|
*/
|
|
32
|
-
export const searchParams:
|
|
35
|
+
export const searchParams: SearchParams;
|
|
33
36
|
|
|
34
37
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
35
38
|
type BaseProps = {};
|
|
@@ -162,9 +165,17 @@ export type NavigateOptions =
|
|
|
162
165
|
search?: string;
|
|
163
166
|
state?: string;
|
|
164
167
|
hash?: string;
|
|
168
|
+
scrollToTop?: ScrollBehavior | false;
|
|
165
169
|
}
|
|
166
170
|
| undefined;
|
|
167
171
|
|
|
172
|
+
export type SearchParams = URLSearchParams & {
|
|
173
|
+
append: (name: string, value: string, options?: { replace?: boolean }) => void;
|
|
174
|
+
delete: (name: string, value?: string, options?: { replace?: boolean }) => void;
|
|
175
|
+
set: (name: string, value: string, options?: { replace?: boolean }) => void;
|
|
176
|
+
sort: (options?: { replace?: boolean }) => void;
|
|
177
|
+
};
|
|
178
|
+
|
|
168
179
|
type NavigateArgs<T extends string> =
|
|
169
180
|
| (PathParams<T> extends never
|
|
170
181
|
? [T] | [T, NavigateOptions]
|
|
@@ -2,15 +2,15 @@ import { SvelteURLSearchParams } from 'svelte/reactivity';
|
|
|
2
2
|
|
|
3
3
|
let searchParams = new SvelteURLSearchParams(globalThis.location.search);
|
|
4
4
|
|
|
5
|
-
/** @type {
|
|
5
|
+
/** @type {import('./index.js').SearchParams} */
|
|
6
6
|
const shell = {
|
|
7
|
-
append(
|
|
8
|
-
searchParams.append(
|
|
9
|
-
updateUrlSearchParams();
|
|
7
|
+
append(name, value, options) {
|
|
8
|
+
searchParams.append(name, value);
|
|
9
|
+
updateUrlSearchParams(options);
|
|
10
10
|
},
|
|
11
|
-
delete(
|
|
12
|
-
searchParams.delete(
|
|
13
|
-
updateUrlSearchParams();
|
|
11
|
+
delete(name, value, options) {
|
|
12
|
+
searchParams.delete(name, value);
|
|
13
|
+
updateUrlSearchParams(options);
|
|
14
14
|
},
|
|
15
15
|
entries() {
|
|
16
16
|
return searchParams.entries();
|
|
@@ -31,13 +31,13 @@ const shell = {
|
|
|
31
31
|
keys() {
|
|
32
32
|
return searchParams.keys();
|
|
33
33
|
},
|
|
34
|
-
set(
|
|
35
|
-
searchParams.set(
|
|
36
|
-
updateUrlSearchParams();
|
|
34
|
+
set(name, value, options) {
|
|
35
|
+
searchParams.set(name, value);
|
|
36
|
+
updateUrlSearchParams(options);
|
|
37
37
|
},
|
|
38
|
-
sort() {
|
|
38
|
+
sort(options) {
|
|
39
39
|
searchParams.sort();
|
|
40
|
-
updateUrlSearchParams();
|
|
40
|
+
updateUrlSearchParams(options);
|
|
41
41
|
},
|
|
42
42
|
toString() {
|
|
43
43
|
return searchParams.toString();
|
|
@@ -60,19 +60,17 @@ export function syncSearchParams() {
|
|
|
60
60
|
if (searchParams.toString() === newSearchParams.toString()) {
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
for (const key of searchParams.keys()) {
|
|
65
|
-
searchParams.delete(key);
|
|
66
|
-
}
|
|
63
|
+
searchParams = new SvelteURLSearchParams();
|
|
67
64
|
for (const [key, value] of newSearchParams.entries()) {
|
|
68
65
|
searchParams.append(key, value);
|
|
69
66
|
}
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
|
|
69
|
+
/** @param {{ replace?: boolean }} [options] */
|
|
70
|
+
function updateUrlSearchParams(options) {
|
|
73
71
|
let url = globalThis.location.origin + globalThis.location.pathname;
|
|
74
72
|
if (searchParams.size > 0) {
|
|
75
73
|
url += '?' + searchParams.toString();
|
|
76
74
|
}
|
|
77
|
-
globalThis.history
|
|
75
|
+
globalThis.history[options?.replace ? 'replaceState' : 'pushState']({}, '', url);
|
|
78
76
|
}
|