sv-router 0.15.0 → 0.16.1
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 +9 -10
- package/package.json +20 -18
- package/src/Router.svelte +8 -2
- package/src/attachments.svelte.js +0 -22
- package/src/gen/generate-router-code.js +8 -2
- package/src/gen/write-router-code.js +20 -4
- package/src/helpers/utils.js +1 -1
- package/src/index.d.ts +1 -13
- package/src/index.js +1 -1
- package/src/search-params.svelte.js +5 -1
package/README.md
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
# sv-router
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/sv-router)
|
|
8
|
-
|
|
8
|
+

|
|
9
|
+

|
|
9
10
|
[](https://codecov.io/github/colinlienard/sv-router)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
Flexible, ergonomic, and complete Svelte 5 router.
|
|
12
13
|
|
|
13
14
|
[Documentation](https://sv-router.vercel.app/) • [Getting Started](https://sv-router.vercel.app/guide/getting-started) • [Reference](https://sv-router.vercel.app/reference)
|
|
14
15
|
|
|
@@ -18,16 +19,14 @@ A feature-rich yet intuitive routing library for Svelte single-page apps.
|
|
|
18
19
|
|
|
19
20
|
## Features
|
|
20
21
|
|
|
21
|
-
- 🔒 **
|
|
22
|
-
-
|
|
23
|
-
-
|
|
22
|
+
- 🔒 **Type-safe navigation**: Catch broken links before you ship.
|
|
23
|
+
- 🗂️ **File-based routing (optional)**: DX of a meta-framework-like approach.
|
|
24
|
+
- 🪶 **Lightweight**: < 5kB gzipped.
|
|
25
|
+
- ⚡ **Performance**: Built-in code splitting and preloading.
|
|
24
26
|
- 🔍 **Reactive search params**: For simpler state management in the URL.
|
|
27
|
+
- 🌿 **Nested routes**: Share layouts across pages.
|
|
25
28
|
- 🛡️ **Hooks**: For navigation guards, data loading, or analytics tracking.
|
|
26
|
-
-
|
|
27
|
-
- 🧩 **Familiar API**: Follows established conventions from popular meta frameworks.
|
|
28
|
-
- 🪶 **Lightweight**: Minimal impact on your bundle size.
|
|
29
|
-
- 🚀 **Made for Svelte 5**: True Svelte 5 implementation with the latest features.
|
|
30
|
-
- #️⃣ **Hash-based routing**: Hash-based routing enables usage in local environments.
|
|
29
|
+
- #️⃣ **Hash-based routing**: Works inside Electron/Tauri.
|
|
31
30
|
|
|
32
31
|
## Getting Started
|
|
33
32
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.16.1",
|
|
4
|
+
"description": "Type-safe routing for Svelte SPAs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
7
7
|
"router",
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
"types": "./src/vite-plugin/index.d.ts"
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
|
-
"bin": "./src/cli/index.js",
|
|
32
31
|
"files": [
|
|
33
32
|
"src"
|
|
34
33
|
],
|
|
@@ -36,30 +35,30 @@
|
|
|
36
35
|
"esm-env": "^1.2.2"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|
|
39
|
-
"@changesets/cli": "^2.
|
|
38
|
+
"@changesets/cli": "^2.31.0",
|
|
40
39
|
"@eslint/js": "^10.0.1",
|
|
41
|
-
"@sveltejs/vite-plugin-svelte": "^7.
|
|
40
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
42
41
|
"@testing-library/jest-dom": "^6.9.1",
|
|
43
42
|
"@testing-library/svelte": "^5.3.1",
|
|
44
43
|
"@testing-library/user-event": "^14.6.1",
|
|
45
|
-
"@types/node": "^24.12.
|
|
46
|
-
"@vitest/coverage-v8": "^4.1.
|
|
47
|
-
"eslint": "^10.0
|
|
44
|
+
"@types/node": "^24.12.3",
|
|
45
|
+
"@vitest/coverage-v8": "^4.1.5",
|
|
46
|
+
"eslint": "^10.3.0",
|
|
48
47
|
"eslint-config-prettier": "^10.1.8",
|
|
49
|
-
"eslint-plugin-simple-import-sort": "^
|
|
50
|
-
"eslint-plugin-svelte": "^3.
|
|
48
|
+
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
49
|
+
"eslint-plugin-svelte": "^3.17.1",
|
|
51
50
|
"eslint-plugin-unicorn": "^64.0.0",
|
|
52
|
-
"globals": "^17.
|
|
53
|
-
"happy-dom": "^20.
|
|
54
|
-
"prettier": "^3.8.
|
|
51
|
+
"globals": "^17.6.0",
|
|
52
|
+
"happy-dom": "^20.9.0",
|
|
53
|
+
"prettier": "^3.8.3",
|
|
55
54
|
"prettier-plugin-jsdoc": "^1.8.0",
|
|
56
55
|
"prettier-plugin-svelte": "^3.5.1",
|
|
57
|
-
"svelte-check": "^4.4.
|
|
56
|
+
"svelte-check": "^4.4.8",
|
|
58
57
|
"type-testing": "^0.2.0",
|
|
59
|
-
"typescript": "^6.0.
|
|
60
|
-
"typescript-eslint": "^8.
|
|
61
|
-
"vite": "^8.0.
|
|
62
|
-
"vitest": "^4.1.
|
|
58
|
+
"typescript": "^6.0.3",
|
|
59
|
+
"typescript-eslint": "^8.59.2",
|
|
60
|
+
"vite": "^8.0.11",
|
|
61
|
+
"vitest": "^4.1.5"
|
|
63
62
|
},
|
|
64
63
|
"peerDependencies": {
|
|
65
64
|
"svelte": "^5"
|
|
@@ -78,5 +77,8 @@
|
|
|
78
77
|
"format": "prettier . --check",
|
|
79
78
|
"format:fix": "prettier . --write",
|
|
80
79
|
"changeset": "changeset"
|
|
80
|
+
},
|
|
81
|
+
"bin": {
|
|
82
|
+
"sv-router": "./src/cli/index.js"
|
|
81
83
|
}
|
|
82
84
|
}
|
package/src/Router.svelte
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import { on } from 'svelte/events';
|
|
3
4
|
import {
|
|
4
5
|
componentTree,
|
|
@@ -7,14 +8,19 @@
|
|
|
7
8
|
onGlobalClick,
|
|
8
9
|
onNavigate,
|
|
9
10
|
} from './create-router.svelte.js';
|
|
11
|
+
import { getUserState } from './helpers/utils.js';
|
|
10
12
|
import RecursiveComponentTree from './RecursiveComponentTree.svelte';
|
|
11
13
|
|
|
12
14
|
/** @type {{ base?: string }} */
|
|
13
15
|
let { base: basename } = $props();
|
|
14
16
|
|
|
15
|
-
init(basename);
|
|
17
|
+
init(untrack(() => basename));
|
|
16
18
|
|
|
17
|
-
onNavigate(
|
|
19
|
+
onNavigate(undefined, {
|
|
20
|
+
search: location.search,
|
|
21
|
+
hash: location.hash,
|
|
22
|
+
state: getUserState(history.state),
|
|
23
|
+
});
|
|
18
24
|
|
|
19
25
|
$effect(() => {
|
|
20
26
|
const cleanup = [
|
|
@@ -23,25 +23,3 @@ export function isActiveLink({ className = 'is-active', startsWith = false } = {
|
|
|
23
23
|
});
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
|
|
27
|
-
/** @type {import('./index.d.ts').IsActiveLinkAction} */
|
|
28
|
-
export function isActiveLinkAction(node, { className = 'is-active', startsWith = false } = {}) {
|
|
29
|
-
if (node.tagName !== 'A') {
|
|
30
|
-
throw new Error('isActiveLink can only be used on <a> elements');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
$effect(() => {
|
|
34
|
-
let pathname;
|
|
35
|
-
if (base.name === '#') {
|
|
36
|
-
pathname = new URL(node.href).hash.slice(1);
|
|
37
|
-
} else {
|
|
38
|
-
pathname = new URL(node.href).pathname;
|
|
39
|
-
}
|
|
40
|
-
const tokens = className.split(' ').filter(Boolean) ?? [];
|
|
41
|
-
if (startsWith ? location.pathname.startsWith(pathname) : location.pathname === pathname) {
|
|
42
|
-
node.classList.add(...tokens);
|
|
43
|
-
} else {
|
|
44
|
-
node.classList.remove(...tokens);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
@@ -165,8 +165,14 @@ function mergeRouteGroup(result, childMap) {
|
|
|
165
165
|
} else if (!Array.isArray(val)) {
|
|
166
166
|
routeWithGroupFiles = { ...val };
|
|
167
167
|
}
|
|
168
|
-
if (layout)
|
|
169
|
-
|
|
168
|
+
if (layout) {
|
|
169
|
+
if (routeWithGroupFiles.layout) {
|
|
170
|
+
routeWithGroupFiles = { '/': routeWithGroupFiles, layout: layout };
|
|
171
|
+
} else {
|
|
172
|
+
routeWithGroupFiles.layout = layout;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (hooks) routeWithGroupFiles.hooks = hooks;
|
|
170
176
|
if (mergedMeta) routeWithGroupFiles.meta = /** @type {string | string[]} */ (mergedMeta);
|
|
171
177
|
if (result[key]) {
|
|
172
178
|
throw new Error(`Route conflict at \`${key}\``);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import { genConfig } from './config.js';
|
|
4
5
|
import { generateRouterCode } from './generate-router-code.js';
|
|
5
6
|
|
|
@@ -10,14 +11,18 @@ export function writeRouterCode() {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
// Write `.router/tsconfig.json` file
|
|
14
|
+
const tsMajor = getTypeScriptMajorVersion();
|
|
15
|
+
const useBaseUrl = tsMajor === undefined || tsMajor < 6;
|
|
16
|
+
let alias = genConfig.routerPath;
|
|
17
|
+
if (!useBaseUrl) {
|
|
18
|
+
alias = alias.replace(genConfig.genCodeDirPath, '.');
|
|
19
|
+
}
|
|
13
20
|
const tsConfig = {
|
|
14
21
|
compilerOptions: {
|
|
15
22
|
module: 'preserve',
|
|
16
23
|
moduleResolution: 'bundler',
|
|
17
|
-
baseUrl: '..',
|
|
18
|
-
paths: {
|
|
19
|
-
[genConfig.genCodeAlias]: [genConfig.routerPath],
|
|
20
|
-
},
|
|
24
|
+
...(useBaseUrl ? { baseUrl: '..' } : {}),
|
|
25
|
+
paths: { [genConfig.genCodeAlias]: [alias] },
|
|
21
26
|
},
|
|
22
27
|
include: [
|
|
23
28
|
'../src/**/*.js',
|
|
@@ -62,3 +67,14 @@ function writeFileIfDifferent(filePath, content) {
|
|
|
62
67
|
return true;
|
|
63
68
|
}
|
|
64
69
|
}
|
|
70
|
+
|
|
71
|
+
function getTypeScriptMajorVersion() {
|
|
72
|
+
try {
|
|
73
|
+
const require = createRequire(process.cwd() + '/package.json');
|
|
74
|
+
const tsPackagePath = require.resolve('typescript/package.json');
|
|
75
|
+
const tsPackage = JSON.parse(fs.readFileSync(tsPackagePath, 'utf8'));
|
|
76
|
+
return Number(tsPackage.version.split('.')[0]);
|
|
77
|
+
} catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/helpers/utils.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -2,19 +2,6 @@ import type { Component, Snippet } from 'svelte';
|
|
|
2
2
|
import type { Action } from 'svelte/action';
|
|
3
3
|
import type { Attachment } from 'svelte/attachments';
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* @deprecated Use the `isActiveLink` [attachment](https://svelte.dev/docs/svelte/@attach) instead.
|
|
7
|
-
*
|
|
8
|
-
* A Svelte action that will add a class to the anchor if its `href` matches the current route. It
|
|
9
|
-
* can have an optional `className` parameter to specify the class to add, otherwise it will
|
|
10
|
-
* default to `is-active`, and an optional `startsWith` parameter.
|
|
11
|
-
*
|
|
12
|
-
* ```svelte
|
|
13
|
-
* <a href={p('/about')} use:isActiveLink={{ className: 'active-link' }}>
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
export const isActiveLinkAction: IsActiveLinkAction;
|
|
17
|
-
|
|
18
5
|
/**
|
|
19
6
|
* A Svelte attachment that will add a class to the anchor if its `href` matches the current route.
|
|
20
7
|
* It can have an optional `className` parameter to specify the class to add, otherwise it will
|
|
@@ -329,6 +316,7 @@ export type SearchParams = Omit<
|
|
|
329
316
|
getAll(name: string): (string | number | boolean)[];
|
|
330
317
|
set(name: string, value: string | number | boolean, options?: { replace?: boolean }): void;
|
|
331
318
|
sort(options?: { replace?: boolean }): void;
|
|
319
|
+
toURLSearchParams(): URLSearchParams;
|
|
332
320
|
values(): (string | number | boolean)[];
|
|
333
321
|
};
|
|
334
322
|
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { isActiveLink
|
|
1
|
+
export { isActiveLink } from './attachments.svelte.js';
|
|
2
2
|
export { blockNavigation, createRouter } from './create-router.svelte.js';
|
|
3
3
|
export { serializeSearch } from './helpers/utils.js';
|
|
4
4
|
export { Navigation } from './navigation.js';
|
|
@@ -47,6 +47,9 @@ const shell = {
|
|
|
47
47
|
values() {
|
|
48
48
|
return [...searchParams.values()].map(parseSearchValue);
|
|
49
49
|
},
|
|
50
|
+
toURLSearchParams() {
|
|
51
|
+
return new URLSearchParams(searchParams);
|
|
52
|
+
},
|
|
50
53
|
get size() {
|
|
51
54
|
return searchParams.size;
|
|
52
55
|
},
|
|
@@ -66,7 +69,8 @@ export function syncSearchParams(search) {
|
|
|
66
69
|
for (const [key, value] of newSearch) {
|
|
67
70
|
searchParams.set(key, value);
|
|
68
71
|
}
|
|
69
|
-
|
|
72
|
+
// eslint-disable-next-line unicorn/no-useless-spread
|
|
73
|
+
for (const key of [...searchParams.keys()]) {
|
|
70
74
|
if (!newSearch.has(key)) {
|
|
71
75
|
searchParams.delete(key);
|
|
72
76
|
}
|