sv-router 0.17.0 → 0.18.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/package.json +15 -14
- package/src/create-router.svelte.js +25 -12
- package/src/gen/generate-router-code.js +4 -3
- package/src/gen/write-router-code.js +8 -6
- package/src/helpers/is-active.js +2 -3
- package/src/helpers/match-route.js +1 -3
- package/src/helpers/preload.js +6 -3
- package/src/helpers/utils.js +15 -16
- package/src/index.d.ts +7 -0
- package/src/search-params.svelte.js +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Type-safe routing for Svelte SPAs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"router",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"src"
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
|
+
"sideEffects": false,
|
|
23
24
|
"exports": {
|
|
24
25
|
".": {
|
|
25
26
|
"import": "./src/index.js",
|
|
@@ -39,24 +40,24 @@
|
|
|
39
40
|
"@eslint/js": "^10.0.1",
|
|
40
41
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
41
42
|
"@testing-library/jest-dom": "^6.9.1",
|
|
42
|
-
"@testing-library/svelte": "^5.
|
|
43
|
+
"@testing-library/svelte": "^5.4.2",
|
|
43
44
|
"@testing-library/user-event": "^14.6.1",
|
|
44
|
-
"@types/node": "^24.
|
|
45
|
-
"@vitest/coverage-v8": "^4.1.
|
|
46
|
-
"eslint": "^10.
|
|
45
|
+
"@types/node": "^24.13.2",
|
|
46
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
47
|
+
"eslint": "^10.6.0",
|
|
47
48
|
"eslint-config-prettier": "^10.1.8",
|
|
48
49
|
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
49
|
-
"eslint-plugin-svelte": "^3.
|
|
50
|
-
"eslint-plugin-unicorn": "^
|
|
51
|
-
"globals": "^17.
|
|
52
|
-
"happy-dom": "^20.
|
|
53
|
-
"oxfmt": "^0.
|
|
54
|
-
"svelte-check": "^4.
|
|
50
|
+
"eslint-plugin-svelte": "^3.20.0",
|
|
51
|
+
"eslint-plugin-unicorn": "^70.0.0",
|
|
52
|
+
"globals": "^17.7.0",
|
|
53
|
+
"happy-dom": "^20.10.6",
|
|
54
|
+
"oxfmt": "^0.57.0",
|
|
55
|
+
"svelte-check": "^4.7.1",
|
|
55
56
|
"type-testing": "^0.2.0",
|
|
56
57
|
"typescript": "^6.0.3",
|
|
57
|
-
"typescript-eslint": "^8.
|
|
58
|
-
"vite": "^8.
|
|
59
|
-
"vitest": "^4.1.
|
|
58
|
+
"typescript-eslint": "^8.62.1",
|
|
59
|
+
"vite": "^8.1.3",
|
|
60
|
+
"vitest": "^4.1.9"
|
|
60
61
|
},
|
|
61
62
|
"peerDependencies": {
|
|
62
63
|
"svelte": "^5"
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
stripBase,
|
|
13
13
|
updatedLocation,
|
|
14
14
|
} from './helpers/utils.js';
|
|
15
|
+
import { validateRoutes } from './helpers/validate-routes.js';
|
|
15
16
|
import { Navigation } from './navigation.js';
|
|
16
17
|
import { syncSearchParams } from './search-params.svelte.js';
|
|
17
18
|
|
|
@@ -53,7 +54,7 @@ let meta = $state({ value: {} });
|
|
|
53
54
|
const navigationBlockers = new Map();
|
|
54
55
|
|
|
55
56
|
let historyIndex = 0;
|
|
56
|
-
let
|
|
57
|
+
let isSkipNextPopstate = false;
|
|
57
58
|
|
|
58
59
|
/** @type {AbortController | null} */
|
|
59
60
|
let currentNavigationController = null;
|
|
@@ -72,7 +73,7 @@ function setBase(basename) {
|
|
|
72
73
|
history.replaceState(
|
|
73
74
|
{ _routerIndex: historyIndex, _userState: history.state ?? null },
|
|
74
75
|
'',
|
|
75
|
-
url.
|
|
76
|
+
url.href,
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
79
|
} else {
|
|
@@ -82,7 +83,7 @@ function setBase(basename) {
|
|
|
82
83
|
history.replaceState(
|
|
83
84
|
{ _routerIndex: historyIndex, _userState: history.state ?? null },
|
|
84
85
|
'',
|
|
85
|
-
url.
|
|
86
|
+
url.href,
|
|
86
87
|
);
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -126,9 +127,7 @@ export function createRouter(r, options = {}) {
|
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
if (DEV && BROWSER) {
|
|
129
|
-
|
|
130
|
-
validateRoutes(routes);
|
|
131
|
-
});
|
|
130
|
+
validateRoutes(routes);
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
preloadOnHover(routes);
|
|
@@ -140,6 +139,9 @@ export function createRouter(r, options = {}) {
|
|
|
140
139
|
async preload(pathname) {
|
|
141
140
|
await preload(routes, pathname);
|
|
142
141
|
},
|
|
142
|
+
resolveMeta(pathname) {
|
|
143
|
+
return matchRoute(pathname, routes).meta;
|
|
144
|
+
},
|
|
143
145
|
route: {
|
|
144
146
|
get params() {
|
|
145
147
|
return params.value;
|
|
@@ -178,7 +180,7 @@ export function createRouter(r, options = {}) {
|
|
|
178
180
|
*/
|
|
179
181
|
async function navigate(path, options = {}) {
|
|
180
182
|
if (typeof path === 'number') {
|
|
181
|
-
|
|
183
|
+
history.go(path);
|
|
182
184
|
return new Navigation(`History entry: ${path}`);
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -213,8 +215,8 @@ export async function onNavigate(path, options = {}) {
|
|
|
213
215
|
throw new Error('Router not initialized: `createRouter` was not called.');
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
if (!path &&
|
|
217
|
-
|
|
218
|
+
if (!path && isSkipNextPopstate) {
|
|
219
|
+
isSkipNextPopstate = false;
|
|
218
220
|
return;
|
|
219
221
|
}
|
|
220
222
|
|
|
@@ -224,7 +226,7 @@ export async function onNavigate(path, options = {}) {
|
|
|
224
226
|
const shouldNavigate = typeof blocker === 'object' ? blocker.onNavigate : blocker;
|
|
225
227
|
if (!(await shouldNavigate())) {
|
|
226
228
|
if (!path && popstateDelta !== 0) {
|
|
227
|
-
|
|
229
|
+
isSkipNextPopstate = true;
|
|
228
230
|
history.go(popstateDelta);
|
|
229
231
|
}
|
|
230
232
|
return;
|
|
@@ -294,10 +296,10 @@ export async function onNavigate(path, options = {}) {
|
|
|
294
296
|
}
|
|
295
297
|
const historyMethod = options.replace ? 'replaceState' : 'pushState';
|
|
296
298
|
if (historyMethod === 'pushState') historyIndex++;
|
|
297
|
-
|
|
299
|
+
history[historyMethod](
|
|
298
300
|
{ _routerIndex: historyIndex, _userState: options.state ?? null },
|
|
299
301
|
'',
|
|
300
|
-
url.
|
|
302
|
+
url.href,
|
|
301
303
|
);
|
|
302
304
|
syncSearchParams(search);
|
|
303
305
|
} else {
|
|
@@ -345,6 +347,17 @@ function getMatchPath(path) {
|
|
|
345
347
|
|
|
346
348
|
/** @param {Event} event */
|
|
347
349
|
export function onGlobalClick(event) {
|
|
350
|
+
const mouseEvent = /** @type {MouseEvent} */ (event);
|
|
351
|
+
if (
|
|
352
|
+
mouseEvent.button !== 0 ||
|
|
353
|
+
mouseEvent.metaKey ||
|
|
354
|
+
mouseEvent.ctrlKey ||
|
|
355
|
+
mouseEvent.shiftKey ||
|
|
356
|
+
mouseEvent.altKey ||
|
|
357
|
+
mouseEvent.defaultPrevented
|
|
358
|
+
)
|
|
359
|
+
return;
|
|
360
|
+
|
|
348
361
|
const anchor = /** @type {HTMLElement} */ (event.target).closest('a');
|
|
349
362
|
if (!anchor) return;
|
|
350
363
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable unicorn/no-unsafe-string-replacement */
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
@@ -174,7 +175,7 @@ function mergeRouteGroup(result, childMap) {
|
|
|
174
175
|
}
|
|
175
176
|
if (hooks) routeWithGroupFiles.hooks = hooks;
|
|
176
177
|
if (mergedMeta) routeWithGroupFiles.meta = /** @type {string | string[]} */ (mergedMeta);
|
|
177
|
-
if (result
|
|
178
|
+
if (Object.hasOwn(result, key)) {
|
|
178
179
|
throw new Error(`Route conflict at \`${key}\``);
|
|
179
180
|
}
|
|
180
181
|
|
|
@@ -223,7 +224,7 @@ export function createRouterCode(routes, routesPath, { allLazy = false, base, js
|
|
|
223
224
|
return result;
|
|
224
225
|
})(routes, routesPath);
|
|
225
226
|
|
|
226
|
-
const imports = [...importsMap
|
|
227
|
+
const imports = [...importsMap].map(([key, value]) => {
|
|
227
228
|
if (value.endsWith('.ts')) {
|
|
228
229
|
value = value.replace('.ts', '');
|
|
229
230
|
}
|
|
@@ -240,7 +241,7 @@ export function createRouterCode(routes, routesPath, { allLazy = false, base, js
|
|
|
240
241
|
'',
|
|
241
242
|
`export const routes = ${stringifiedRoutes};`,
|
|
242
243
|
...(js ? [] : ['export type Routes = typeof routes;']),
|
|
243
|
-
`export const { p, navigate, isActive, preload, route } = createRouter(routes${
|
|
244
|
+
`export const { p, navigate, isActive, preload, resolveMeta, route } = createRouter(routes${
|
|
244
245
|
base === undefined ? '' : `, { base: '${base}' }`
|
|
245
246
|
});`,
|
|
246
247
|
'',
|
|
@@ -12,16 +12,16 @@ export function writeRouterCode() {
|
|
|
12
12
|
|
|
13
13
|
// Write `.router/tsconfig.json` file
|
|
14
14
|
const tsMajor = getTypeScriptMajorVersion();
|
|
15
|
-
const
|
|
15
|
+
const isBaseUrl = tsMajor === undefined || tsMajor < 6;
|
|
16
16
|
let alias = genConfig.routerPath;
|
|
17
|
-
if (!
|
|
17
|
+
if (!isBaseUrl) {
|
|
18
18
|
alias = alias.replace(genConfig.genCodeDirPath, '.');
|
|
19
19
|
}
|
|
20
20
|
const tsConfig = {
|
|
21
21
|
compilerOptions: {
|
|
22
22
|
module: 'preserve',
|
|
23
23
|
moduleResolution: 'bundler',
|
|
24
|
-
...(
|
|
24
|
+
...(isBaseUrl && { baseUrl: '..' }),
|
|
25
25
|
paths: { [genConfig.genCodeAlias]: [alias] },
|
|
26
26
|
},
|
|
27
27
|
include: [
|
|
@@ -63,10 +63,12 @@ export function writeRouterCode() {
|
|
|
63
63
|
* @param {string} content
|
|
64
64
|
*/
|
|
65
65
|
function writeFileIfDifferent(filePath, content) {
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
return true;
|
|
66
|
+
if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content) {
|
|
67
|
+
return;
|
|
69
68
|
}
|
|
69
|
+
|
|
70
|
+
fs.writeFileSync(filePath, content);
|
|
71
|
+
return true;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
function getTypeScriptMajorVersion() {
|
package/src/helpers/is-active.js
CHANGED
|
@@ -34,9 +34,8 @@ function compare(compareFn, pathname, params) {
|
|
|
34
34
|
if (params) {
|
|
35
35
|
if (base.name === '#') {
|
|
36
36
|
return compareFn(location.pathname, constructPath(pathname, params).replace('/#', ''));
|
|
37
|
-
} else {
|
|
38
|
-
return compareFn(location.pathname, constructPath(pathname, params));
|
|
39
37
|
}
|
|
38
|
+
return compareFn(location.pathname, constructPath(pathname, params));
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
const pathParts = pathname.split('/').slice(1);
|
|
@@ -45,10 +44,10 @@ function compare(compareFn, pathname, params) {
|
|
|
45
44
|
return false;
|
|
46
45
|
}
|
|
47
46
|
for (const [index, pathPart] of pathParts.entries()) {
|
|
48
|
-
const routePart = routeParts[index];
|
|
49
47
|
if (pathPart.startsWith(':')) {
|
|
50
48
|
continue;
|
|
51
49
|
}
|
|
50
|
+
const routePart = routeParts[index];
|
|
52
51
|
if (pathPart !== routePart) {
|
|
53
52
|
return false;
|
|
54
53
|
}
|
|
@@ -84,11 +84,9 @@ function tryMatch(route, pathParts, pathname, routes, baseMeta) {
|
|
|
84
84
|
|
|
85
85
|
/** @type {Record<string, string>} */
|
|
86
86
|
const params = {};
|
|
87
|
-
/** @type {boolean} */
|
|
88
|
-
let breakFromLayouts;
|
|
89
87
|
|
|
90
88
|
for (let [index, routePart] of routeParts.entries()) {
|
|
91
|
-
breakFromLayouts = routePart.startsWith('(') && routePart.endsWith(')');
|
|
89
|
+
const breakFromLayouts = routePart.startsWith('(') && routePart.endsWith(')');
|
|
92
90
|
if (breakFromLayouts) {
|
|
93
91
|
routePart = routePart.slice(1, -1);
|
|
94
92
|
}
|
package/src/helpers/preload.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable unicorn/no-break-in-nested-loop */
|
|
1
2
|
import { matchRoute } from './match-route.js';
|
|
2
3
|
import { parseSearch, resolveRouteComponents, stripBase } from './utils.js';
|
|
3
4
|
|
|
@@ -139,10 +140,12 @@ export function preloadOnHover(routes) {
|
|
|
139
140
|
|
|
140
141
|
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
141
142
|
for (const entry of entries) {
|
|
142
|
-
if (entry.isIntersecting) {
|
|
143
|
-
|
|
144
|
-
anchorPreload(/** @type {HTMLAnchorElement} */ (entry.target));
|
|
143
|
+
if (!entry.isIntersecting) {
|
|
144
|
+
continue;
|
|
145
145
|
}
|
|
146
|
+
|
|
147
|
+
intersectionObserver.unobserve(entry.target);
|
|
148
|
+
anchorPreload(/** @type {HTMLAnchorElement} */ (entry.target));
|
|
146
149
|
}
|
|
147
150
|
});
|
|
148
151
|
|
package/src/helpers/utils.js
CHANGED
|
@@ -8,7 +8,7 @@ import { base } from '../create-router.svelte.js';
|
|
|
8
8
|
export function constructPath(path, params) {
|
|
9
9
|
if (params) {
|
|
10
10
|
for (const key in params) {
|
|
11
|
-
path = path.replace(`:${key}`, encodeURIComponent(params[key]));
|
|
11
|
+
path = path.replace(`:${key}`, () => encodeURIComponent(params[key]));
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -17,7 +17,8 @@ export function constructPath(path, params) {
|
|
|
17
17
|
return '/#/';
|
|
18
18
|
}
|
|
19
19
|
return join('#', path);
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
|
+
if (base.name) {
|
|
21
22
|
return join(base.name, path);
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -54,16 +55,15 @@ export function resolveRouteComponents(input) {
|
|
|
54
55
|
* @param {import('../index.d.ts').RouteComponent} input
|
|
55
56
|
* @returns {Promise<import('svelte').Component>}
|
|
56
57
|
*/
|
|
57
|
-
function resolveRouteComponent(input) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
} else {
|
|
64
|
-
resolve(input);
|
|
58
|
+
async function resolveRouteComponent(input) {
|
|
59
|
+
if (isLazyImport(input)) {
|
|
60
|
+
const module = await input();
|
|
61
|
+
if (!module) {
|
|
62
|
+
throw new Error('Failed to load route component: the lazy import resolved to nothing');
|
|
65
63
|
}
|
|
66
|
-
|
|
64
|
+
return module.default;
|
|
65
|
+
}
|
|
66
|
+
return input;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -112,12 +112,11 @@ export function getUserState(state) {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export function updatedLocation() {
|
|
115
|
-
const pathname =
|
|
116
|
-
|
|
117
|
-
const hash = base.name === '#' ? '' : globalThis.location.hash;
|
|
115
|
+
const pathname = base.name === '#' ? location.hash.slice(1) : location.pathname;
|
|
116
|
+
const hash = base.name === '#' ? '' : location.hash;
|
|
118
117
|
return {
|
|
119
118
|
pathname,
|
|
120
|
-
search:
|
|
119
|
+
search: location.search,
|
|
121
120
|
state: getUserState(history.state),
|
|
122
121
|
hash,
|
|
123
122
|
};
|
|
@@ -161,7 +160,7 @@ export function parseSearch(value) {
|
|
|
161
160
|
if (typeof value === 'string') {
|
|
162
161
|
const searchParams = new URLSearchParams(value);
|
|
163
162
|
return Object.fromEntries(
|
|
164
|
-
[...searchParams
|
|
163
|
+
[...searchParams].map(([key, value]) => [key, parseSearchValue(value)]),
|
|
165
164
|
);
|
|
166
165
|
}
|
|
167
166
|
|
package/src/index.d.ts
CHANGED
|
@@ -227,6 +227,13 @@ export type RouterApi<T extends Routes> = {
|
|
|
227
227
|
*/
|
|
228
228
|
preload<U extends Path<T>>(path: U): Promise<void>;
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Resolves the metadata for the given route path by merging meta from all ancestor route levels.
|
|
232
|
+
*
|
|
233
|
+
* @param path The route to resolve meta for.
|
|
234
|
+
*/
|
|
235
|
+
resolveMeta<U extends Path<T>>(path: U): RouteMeta;
|
|
236
|
+
|
|
230
237
|
route: {
|
|
231
238
|
/**
|
|
232
239
|
* An object containing the parameters of the current route.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SvelteURLSearchParams } from 'svelte/reactivity';
|
|
2
2
|
import { parseSearchValue } from './helpers/utils.js';
|
|
3
3
|
|
|
4
|
-
let searchParams = new SvelteURLSearchParams(
|
|
4
|
+
let searchParams = new SvelteURLSearchParams(location.search);
|
|
5
5
|
|
|
6
6
|
/** @type {import('./index.js').SearchParams} */
|
|
7
7
|
const shell = {
|
|
@@ -38,6 +38,7 @@ const shell = {
|
|
|
38
38
|
updateUrlSearchParams(options);
|
|
39
39
|
},
|
|
40
40
|
sort(options) {
|
|
41
|
+
// eslint-disable-next-line unicorn/require-array-sort-compare
|
|
41
42
|
searchParams.sort();
|
|
42
43
|
updateUrlSearchParams(options);
|
|
43
44
|
},
|
|
@@ -82,7 +83,7 @@ export function syncSearchParams(search) {
|
|
|
82
83
|
|
|
83
84
|
/** @param {{ replace?: boolean }} [options] */
|
|
84
85
|
function updateUrlSearchParams(options) {
|
|
85
|
-
const url = new URL(
|
|
86
|
+
const url = new URL(location.toString());
|
|
86
87
|
url.search = searchParams.toString();
|
|
87
|
-
|
|
88
|
+
history[options?.replace ? 'replaceState' : 'pushState']({}, '', url);
|
|
88
89
|
}
|