sv-router 0.9.0 → 0.10.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/package.json +14 -14
- package/src/create-router.svelte.js +6 -1
- package/src/gen/generate-router-code.js +43 -3
- package/src/helpers/preload.js +158 -13
- package/src/helpers/utils.js +3 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Modern Svelte Routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -36,28 +36,28 @@
|
|
|
36
36
|
"esm-env": "^1.2.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@changesets/cli": "^2.29.
|
|
40
|
-
"@eslint/js": "^9.
|
|
41
|
-
"@sveltejs/vite-plugin-svelte": "^6.1
|
|
42
|
-
"@testing-library/jest-dom": "^6.
|
|
39
|
+
"@changesets/cli": "^2.29.7",
|
|
40
|
+
"@eslint/js": "^9.36.0",
|
|
41
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
42
|
+
"@testing-library/jest-dom": "^6.9.0",
|
|
43
43
|
"@testing-library/svelte": "^5.2.8",
|
|
44
44
|
"@testing-library/user-event": "^14.6.1",
|
|
45
|
-
"@types/node": "^24.
|
|
46
|
-
"eslint": "^9.
|
|
45
|
+
"@types/node": "^24.6.0",
|
|
46
|
+
"eslint": "^9.36.0",
|
|
47
47
|
"eslint-config-prettier": "^10.1.8",
|
|
48
48
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
49
|
-
"eslint-plugin-svelte": "^3.
|
|
50
|
-
"eslint-plugin-unicorn": "^
|
|
51
|
-
"globals": "^16.
|
|
52
|
-
"happy-dom": "^
|
|
49
|
+
"eslint-plugin-svelte": "^3.12.4",
|
|
50
|
+
"eslint-plugin-unicorn": "^61.0.2",
|
|
51
|
+
"globals": "^16.4.0",
|
|
52
|
+
"happy-dom": "^19.0.2",
|
|
53
53
|
"prettier": "^3.6.2",
|
|
54
54
|
"prettier-plugin-jsdoc": "^1.3.3",
|
|
55
55
|
"prettier-plugin-svelte": "^3.4.0",
|
|
56
|
-
"svelte-check": "^4.3.
|
|
56
|
+
"svelte-check": "^4.3.2",
|
|
57
57
|
"type-testing": "^0.2.0",
|
|
58
58
|
"typescript": "^5.9.2",
|
|
59
|
-
"typescript-eslint": "^8.
|
|
60
|
-
"vite": "^7.1.
|
|
59
|
+
"typescript-eslint": "^8.45.0",
|
|
60
|
+
"vite": "^7.1.7",
|
|
61
61
|
"vitest": "^3.2.4"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
@@ -242,7 +242,12 @@ export function onGlobalClick(event) {
|
|
|
242
242
|
const anchor = /** @type {HTMLElement} */ (event.target).closest('a');
|
|
243
243
|
if (!anchor) return;
|
|
244
244
|
|
|
245
|
-
if (
|
|
245
|
+
if (
|
|
246
|
+
anchor.hasAttribute('target') ||
|
|
247
|
+
anchor.hasAttribute('download') ||
|
|
248
|
+
!anchor.hasAttribute('href')
|
|
249
|
+
)
|
|
250
|
+
return;
|
|
246
251
|
|
|
247
252
|
const url = new URL(anchor.href);
|
|
248
253
|
const currentOrigin = globalThis.location.origin;
|
|
@@ -110,8 +110,15 @@ export function createRouteMap(fileTree, prefix = '') {
|
|
|
110
110
|
result['/' + filePathToRoute(entry.replace('.svelte', ''))] = prefix + entry;
|
|
111
111
|
} else {
|
|
112
112
|
const entryName = filePathToRoute(entry.name);
|
|
113
|
-
const
|
|
114
|
-
|
|
113
|
+
const isRouteGroup = /^_[^_[]/.test(entry.name);
|
|
114
|
+
|
|
115
|
+
if (isRouteGroup) {
|
|
116
|
+
const childMap = createRouteMap(entry.tree, prefix + entryName + '/');
|
|
117
|
+
mergeRouteGroup(result, childMap);
|
|
118
|
+
} else {
|
|
119
|
+
const paramFolder = entryName.replace(/^\[(.*)\]$/, ':$1');
|
|
120
|
+
result['/' + paramFolder] = createRouteMap(entry.tree, prefix + entryName + '/');
|
|
121
|
+
}
|
|
115
122
|
}
|
|
116
123
|
}
|
|
117
124
|
return result;
|
|
@@ -127,6 +134,38 @@ function filePathToRoute(filename) {
|
|
|
127
134
|
return filename.replaceAll(/\.(?!\.\.)/g, '/');
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @param {GeneratedRoutes} result
|
|
139
|
+
* @param {GeneratedRoutes} childMap
|
|
140
|
+
*/
|
|
141
|
+
function mergeRouteGroup(result, childMap) {
|
|
142
|
+
const layout = childMap.layout;
|
|
143
|
+
const hooks = childMap.hooks;
|
|
144
|
+
const meta = childMap.meta;
|
|
145
|
+
|
|
146
|
+
for (const [key, val] of Object.entries(childMap)) {
|
|
147
|
+
if (key === 'layout' || key === 'hooks' || key === 'meta') {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @type {GeneratedRoutes} */
|
|
152
|
+
let routeWithGroupFiles = {};
|
|
153
|
+
if (typeof val === 'string') {
|
|
154
|
+
routeWithGroupFiles = { '/': val };
|
|
155
|
+
} else {
|
|
156
|
+
routeWithGroupFiles = { ...val };
|
|
157
|
+
}
|
|
158
|
+
if (layout) routeWithGroupFiles.layout = layout;
|
|
159
|
+
if (hooks) routeWithGroupFiles.hooks = hooks;
|
|
160
|
+
if (meta) routeWithGroupFiles.meta = meta;
|
|
161
|
+
if (result[key]) {
|
|
162
|
+
throw new Error(`Route conflict at \`${key}\``);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
result[key] = routeWithGroupFiles;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
130
169
|
/**
|
|
131
170
|
* @param {GeneratedRoutes} routes
|
|
132
171
|
* @param {string} routesPath
|
|
@@ -177,7 +216,7 @@ export function createRouterCode(routes, routesPath, { allLazy = false, js = fal
|
|
|
177
216
|
`import { createRouter } from 'sv-router';`,
|
|
178
217
|
...imports,
|
|
179
218
|
'',
|
|
180
|
-
`const routes = ${stringifiedRoutes};`,
|
|
219
|
+
`export const routes = ${stringifiedRoutes};`,
|
|
181
220
|
...(js ? [] : ['export type Routes = typeof routes;']),
|
|
182
221
|
'export const { p, navigate, isActive, preload, route } = createRouter(routes);',
|
|
183
222
|
'',
|
|
@@ -214,6 +253,7 @@ export function pathToCorrectCasing(value) {
|
|
|
214
253
|
parts.push(...lastPart.split('-'));
|
|
215
254
|
|
|
216
255
|
const uppercased = parts.map((part, index) => {
|
|
256
|
+
part = part.replace(/^_+/, '');
|
|
217
257
|
if (index === 0 && (lastPart === 'hooks' || lastPart === 'meta')) return part;
|
|
218
258
|
part = part.replace(/^\[(.*)\]$/, '$1');
|
|
219
259
|
return part.charAt(0).toUpperCase() + part.slice(1);
|
package/src/helpers/preload.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { matchRoute } from './match-route.js';
|
|
2
|
-
import { parseSearch, resolveRouteComponents } from './utils.js';
|
|
2
|
+
import { parseSearch, resolveRouteComponents, stripBase } from './utils.js';
|
|
3
|
+
|
|
4
|
+
const PREDICT_CONE_LENGTH = 200;
|
|
5
|
+
const PREDICT_CONE_ANGLE = Math.PI / 6;
|
|
6
|
+
const PREDICT_TIMEOUT = 50;
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* @param {import('../index.js').Routes} routes
|
|
@@ -19,30 +23,171 @@ export async function preload(routes, path, options) {
|
|
|
19
23
|
await resolveRouteComponents(match ? [...layouts, match] : layouts);
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
/** @type {Set<HTMLAnchorElement>} */
|
|
22
27
|
const linkSet = new Set();
|
|
28
|
+
/** @type {Set<HTMLAnchorElement>} */
|
|
29
|
+
const predictedLinks = new Set();
|
|
23
30
|
|
|
24
31
|
/** @param {import('../index.js').Routes} routes */
|
|
25
32
|
export function preloadOnHover(routes) {
|
|
33
|
+
/** @param {HTMLAnchorElement} link */
|
|
34
|
+
function anchorPreload(link) {
|
|
35
|
+
const href = link.getAttribute('href');
|
|
36
|
+
if (!href) return;
|
|
37
|
+
const url = new URL(link.href);
|
|
38
|
+
const pathname = stripBase(url.pathname);
|
|
39
|
+
const { replace, state } = link.dataset;
|
|
40
|
+
preload(routes, pathname, {
|
|
41
|
+
replace: replace === '' || replace === 'true',
|
|
42
|
+
search: url.search,
|
|
43
|
+
state,
|
|
44
|
+
hash: url.hash,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
49
|
+
let throttleTimer = null;
|
|
50
|
+
function pointerMoveListener(/** @type {PointerEvent} */ event) {
|
|
51
|
+
if (!event.getPredictedEvents || throttleTimer) return;
|
|
52
|
+
throttleTimer = setTimeout(() => {
|
|
53
|
+
throttleTimer = null;
|
|
54
|
+
}, PREDICT_TIMEOUT);
|
|
55
|
+
|
|
56
|
+
if (predictedLinks.size === 0) {
|
|
57
|
+
document.removeEventListener('pointermove', pointerMoveListener);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const predictedEvents = event.getPredictedEvents();
|
|
62
|
+
const lastPredicted = predictedEvents.at(-1);
|
|
63
|
+
if (!lastPredicted) return;
|
|
64
|
+
|
|
65
|
+
const currentX = event.clientX;
|
|
66
|
+
const currentY = event.clientY;
|
|
67
|
+
const dx = lastPredicted.clientX - currentX;
|
|
68
|
+
const dy = lastPredicted.clientY - currentY;
|
|
69
|
+
|
|
70
|
+
const distance = Math.hypot(dx, dy);
|
|
71
|
+
if (distance < 2) return;
|
|
72
|
+
const dirX = dx / distance;
|
|
73
|
+
const dirY = dy / distance;
|
|
74
|
+
|
|
75
|
+
// Visualize the cone (comment out when not debugging)
|
|
76
|
+
/*
|
|
77
|
+
const canvas = document.createElement('canvas');
|
|
78
|
+
canvas.style.position = 'fixed';
|
|
79
|
+
canvas.style.left = '0';
|
|
80
|
+
canvas.style.top = '0';
|
|
81
|
+
canvas.style.width = '100%';
|
|
82
|
+
canvas.style.height = '100%';
|
|
83
|
+
canvas.style.pointerEvents = 'none';
|
|
84
|
+
canvas.style.zIndex = '99999';
|
|
85
|
+
canvas.width = window.innerWidth;
|
|
86
|
+
canvas.height = window.innerHeight;
|
|
87
|
+
document.body.append(canvas);
|
|
88
|
+
const ctx = canvas.getContext('2d');
|
|
89
|
+
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
|
|
90
|
+
ctx.strokeStyle = 'rgba(255, 0, 0, 0.6)';
|
|
91
|
+
ctx.lineWidth = 2;
|
|
92
|
+
ctx.beginPath();
|
|
93
|
+
ctx.moveTo(currentX, currentY);
|
|
94
|
+
const leftAngle = Math.atan2(dirY, dirX) - PREDICT_CONE_ANGLE;
|
|
95
|
+
const rightAngle = Math.atan2(dirY, dirX) + PREDICT_CONE_ANGLE;
|
|
96
|
+
const leftX = currentX + Math.cos(leftAngle) * PREDICT_CONE_LENGTH;
|
|
97
|
+
const leftY = currentY + Math.sin(leftAngle) * PREDICT_CONE_LENGTH;
|
|
98
|
+
ctx.lineTo(leftX, leftY);
|
|
99
|
+
ctx.arc(currentX, currentY, PREDICT_CONE_LENGTH, leftAngle, rightAngle, false);
|
|
100
|
+
ctx.lineTo(currentX, currentY);
|
|
101
|
+
ctx.closePath();
|
|
102
|
+
ctx.fill();
|
|
103
|
+
ctx.stroke();
|
|
104
|
+
setTimeout(() => canvas.remove(), PREDICT_TIMEOUT);
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
outer: for (const link of predictedLinks) {
|
|
108
|
+
if (!link.isConnected) {
|
|
109
|
+
predictedLinks.delete(link);
|
|
110
|
+
}
|
|
111
|
+
const rect = link.getBoundingClientRect();
|
|
112
|
+
const points = [
|
|
113
|
+
{ x: rect.left, y: rect.top },
|
|
114
|
+
{ x: rect.right, y: rect.top },
|
|
115
|
+
{ x: rect.left, y: rect.bottom },
|
|
116
|
+
{ x: rect.right, y: rect.bottom },
|
|
117
|
+
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const point of points) {
|
|
121
|
+
const toPointX = point.x - currentX;
|
|
122
|
+
const toPointY = point.y - currentY;
|
|
123
|
+
const distToPoint = Math.hypot(toPointX, toPointY);
|
|
124
|
+
if (distToPoint > PREDICT_CONE_LENGTH || distToPoint < 0.001) continue;
|
|
125
|
+
|
|
126
|
+
const toPointDirX = toPointX / distToPoint;
|
|
127
|
+
const toPointDirY = toPointY / distToPoint;
|
|
128
|
+
const dotProduct = dirX * toPointDirX + dirY * toPointDirY;
|
|
129
|
+
const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct)));
|
|
130
|
+
|
|
131
|
+
if (angle <= PREDICT_CONE_ANGLE) {
|
|
132
|
+
anchorPreload(link);
|
|
133
|
+
predictedLinks.delete(link);
|
|
134
|
+
continue outer;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const intersectionObserver = new IntersectionObserver((entries) => {
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (entry.isIntersecting) {
|
|
143
|
+
intersectionObserver.unobserve(entry.target);
|
|
144
|
+
anchorPreload(/** @type {HTMLAnchorElement} */ (entry.target));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
26
149
|
const observer = new MutationObserver(() => {
|
|
27
150
|
const links = /** @type {NodeListOf<HTMLAnchorElement>} */ (
|
|
28
151
|
document.querySelectorAll('a[data-preload]')
|
|
29
152
|
);
|
|
153
|
+
|
|
30
154
|
for (const link of links) {
|
|
31
155
|
if (linkSet.has(link)) continue;
|
|
32
156
|
linkSet.add(link);
|
|
33
157
|
|
|
34
|
-
link.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
158
|
+
switch (link.dataset.preload) {
|
|
159
|
+
case '':
|
|
160
|
+
case 'hover': {
|
|
161
|
+
link.addEventListener('mouseenter', function callback() {
|
|
162
|
+
link.removeEventListener('mouseenter', callback);
|
|
163
|
+
anchorPreload(link);
|
|
164
|
+
});
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'predict': {
|
|
168
|
+
if (predictedLinks.size === 0) {
|
|
169
|
+
document.addEventListener('pointermove', pointerMoveListener);
|
|
170
|
+
}
|
|
171
|
+
predictedLinks.add(link);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'viewport': {
|
|
175
|
+
intersectionObserver.observe(link);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
default: {
|
|
179
|
+
console.warn(
|
|
180
|
+
`Unknown preload strategy \`${link.dataset.preload}\` on`,
|
|
181
|
+
link,
|
|
182
|
+
'\nAvailable strategies are: hover, viewport, predict',
|
|
183
|
+
);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
link.addEventListener('focus', function callback() {
|
|
189
|
+
link.removeEventListener('focus', callback);
|
|
190
|
+
anchorPreload(link);
|
|
46
191
|
});
|
|
47
192
|
}
|
|
48
193
|
});
|
package/src/helpers/utils.js
CHANGED
|
@@ -133,6 +133,9 @@ export function serializeSearch(value) {
|
|
|
133
133
|
const stringValues = Object.fromEntries(
|
|
134
134
|
Object.entries(value).map(([key, value]) => [key, String(value)]),
|
|
135
135
|
);
|
|
136
|
+
if (Object.keys(stringValues).length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
136
139
|
const urlSearchParams = new URLSearchParams(stringValues);
|
|
137
140
|
return '?' + urlSearchParams.toString();
|
|
138
141
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -12,6 +12,9 @@ import type { Action } from 'svelte/action';
|
|
|
12
12
|
*/
|
|
13
13
|
export const isActiveLink: IsActiveLink;
|
|
14
14
|
|
|
15
|
+
/** Create a search string from the search object that is provided in hooks. */
|
|
16
|
+
export function serializeSearch(search: Search): string | undefined;
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* Setup a new router instance with the given routes.
|
|
17
20
|
*
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { isActiveLink } from './actions.svelte.js';
|
|
2
2
|
export { createRouter } from './create-router.svelte.js';
|
|
3
|
+
export { serializeSearch } from './helpers/utils.js';
|
|
3
4
|
export { default as Router } from './Router.svelte';
|
|
4
5
|
export { searchParams } from './search-params.svelte.js';
|