sv-router 0.5.0 → 0.6.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 +12 -12
- package/src/create-router.svelte.js +14 -2
- package/src/gen/generate-router-code.js +20 -4
- package/src/helpers/is-active.js +5 -3
- package/src/helpers/match-route.js +13 -2
- package/src/helpers/utils.js +1 -1
- package/src/helpers/validate-routes.js +4 -1
- package/src/index.d.ts +31 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sv-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Modern Svelte Routing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -36,24 +36,24 @@
|
|
|
36
36
|
"esm-env": "^1.2.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@changesets/cli": "^2.29.
|
|
40
|
-
"@eslint/js": "^9.
|
|
41
|
-
"@types/node": "^22.15.
|
|
42
|
-
"eslint": "^9.
|
|
43
|
-
"eslint-config-prettier": "^10.1.
|
|
39
|
+
"@changesets/cli": "^2.29.4",
|
|
40
|
+
"@eslint/js": "^9.28.0",
|
|
41
|
+
"@types/node": "^22.15.30",
|
|
42
|
+
"eslint": "^9.28.0",
|
|
43
|
+
"eslint-config-prettier": "^10.1.5",
|
|
44
44
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
45
|
-
"eslint-plugin-svelte": "^3.
|
|
45
|
+
"eslint-plugin-svelte": "^3.9.1",
|
|
46
46
|
"eslint-plugin-unicorn": "^59.0.1",
|
|
47
|
-
"globals": "^16.
|
|
47
|
+
"globals": "^16.2.0",
|
|
48
48
|
"prettier": "^3.5.3",
|
|
49
49
|
"prettier-plugin-jsdoc": "^1.3.2",
|
|
50
|
-
"prettier-plugin-svelte": "^3.
|
|
51
|
-
"svelte-check": "^4.1
|
|
50
|
+
"prettier-plugin-svelte": "^3.4.0",
|
|
51
|
+
"svelte-check": "^4.2.1",
|
|
52
52
|
"type-testing": "^0.2.0",
|
|
53
53
|
"typescript": "^5.8.3",
|
|
54
|
-
"typescript-eslint": "^8.
|
|
54
|
+
"typescript-eslint": "^8.33.1",
|
|
55
55
|
"vite": "^6.3.5",
|
|
56
|
-
"vitest": "^3.
|
|
56
|
+
"vitest": "^3.2.2"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"svelte": "^5"
|
|
@@ -16,6 +16,8 @@ export let params = $state({ value: {} });
|
|
|
16
16
|
|
|
17
17
|
export let location = $state(updatedLocation());
|
|
18
18
|
|
|
19
|
+
let meta = $state({ value: {} });
|
|
20
|
+
|
|
19
21
|
let navigationIndex = 0;
|
|
20
22
|
let pendingNavigationIndex = 0;
|
|
21
23
|
|
|
@@ -51,6 +53,12 @@ export function createRouter(r) {
|
|
|
51
53
|
get params() {
|
|
52
54
|
return params.value;
|
|
53
55
|
},
|
|
56
|
+
getParams(pathname) {
|
|
57
|
+
if (!isActive(pathname)) {
|
|
58
|
+
throw new Error(`\`${pathname}\` does not match the current route`);
|
|
59
|
+
}
|
|
60
|
+
return params.value;
|
|
61
|
+
},
|
|
54
62
|
get pathname() {
|
|
55
63
|
return /** @type {import('./index.d.ts').Path<T>} */ (location.pathname);
|
|
56
64
|
},
|
|
@@ -63,6 +71,9 @@ export function createRouter(r) {
|
|
|
63
71
|
get hash() {
|
|
64
72
|
return location.hash;
|
|
65
73
|
},
|
|
74
|
+
get meta() {
|
|
75
|
+
return meta.value;
|
|
76
|
+
},
|
|
66
77
|
},
|
|
67
78
|
};
|
|
68
79
|
}
|
|
@@ -104,7 +115,7 @@ export async function onNavigate(path, options = {}) {
|
|
|
104
115
|
if (base.name && matchPath.startsWith(base.name)) {
|
|
105
116
|
matchPath = matchPath.slice(base.name.length) || '/';
|
|
106
117
|
}
|
|
107
|
-
const { match, layouts, hooks, params: newParams } = matchRoute(matchPath, routes);
|
|
118
|
+
const { match, layouts, hooks, meta: newMeta, params: newParams } = matchRoute(matchPath, routes);
|
|
108
119
|
|
|
109
120
|
for (const { beforeLoad } of hooks) {
|
|
110
121
|
try {
|
|
@@ -140,7 +151,8 @@ export async function onNavigate(path, options = {}) {
|
|
|
140
151
|
} else {
|
|
141
152
|
componentTree.value = routeComponents;
|
|
142
153
|
}
|
|
143
|
-
params.value = newParams
|
|
154
|
+
params.value = newParams;
|
|
155
|
+
meta.value = newMeta;
|
|
144
156
|
syncSearchParams();
|
|
145
157
|
Object.assign(location, updatedLocation());
|
|
146
158
|
|
|
@@ -14,6 +14,7 @@ const PARAM_FILENAME_REGEX = /(?<=[/.]|^)\(?\[([\w-]+)\]\)?(\.lazy)?\.svelte$/;
|
|
|
14
14
|
const CATCH_ALL_FILENAME_REGEX = /(?<=[/.]|^)\(?\[\.\.\.([\w-]+)\]\)?(\.lazy)?\.svelte$/; // [...any].svelte, [...any].lazy.svelte, ([...any]).svelte
|
|
15
15
|
const OUT_OF_LAYOUT_FILENAME_REGEX = /(?<=[/.]|^)\(\[\.?\.?\.?([\w-]+)\]\)(\.lazy)?\.svelte$/; // ([any]).svelte, ([...any]).lazy.svelte
|
|
16
16
|
const HOOKS_FILENAME_REGEX = /(?<=[/.]|^)(hooks)(\.svelte)?\.(js|ts)$/; // hooks.js, hooks.svelte.js, hooks.ts, hooks.svelte.ts
|
|
17
|
+
const META_FILENAME_REGEX = /(?<=[/.]|^)(meta)(\.svelte)?\.(js|ts)$/; // meta.js, meta.svelte.js, meta.ts, meta.svelte.ts
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* @param {string} routesPath
|
|
@@ -44,7 +45,11 @@ export function buildFileTree(routesPath) {
|
|
|
44
45
|
tree.push({ name: entry, tree: buildFileTree(path.join(routesPath, entry)) });
|
|
45
46
|
continue;
|
|
46
47
|
}
|
|
47
|
-
if (
|
|
48
|
+
if (
|
|
49
|
+
!entry.endsWith('.svelte') &&
|
|
50
|
+
!HOOKS_FILENAME_REGEX.test(entry) &&
|
|
51
|
+
!META_FILENAME_REGEX.test(entry)
|
|
52
|
+
) {
|
|
48
53
|
continue;
|
|
49
54
|
}
|
|
50
55
|
tree.push(entry);
|
|
@@ -67,6 +72,10 @@ export function createRouteMap(fileTree, prefix = '') {
|
|
|
67
72
|
result['hooks'] = prefix + entry;
|
|
68
73
|
continue;
|
|
69
74
|
}
|
|
75
|
+
if (META_FILENAME_REGEX.test(entry)) {
|
|
76
|
+
result['meta'] = prefix + entry;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
70
79
|
continue;
|
|
71
80
|
}
|
|
72
81
|
|
|
@@ -101,7 +110,8 @@ export function createRouteMap(fileTree, prefix = '') {
|
|
|
101
110
|
result['/' + filePathToRoute(entry.replace('.svelte', ''))] = prefix + entry;
|
|
102
111
|
} else {
|
|
103
112
|
const entryName = filePathToRoute(entry.name);
|
|
104
|
-
|
|
113
|
+
const paramFolder = entryName.replace(/^\[(.*)\]$/, ':$1');
|
|
114
|
+
result['/' + paramFolder] = createRouteMap(entry.tree, prefix + entryName + '/');
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
return result;
|
|
@@ -137,7 +147,11 @@ export function createRouterCode(routes, routesPath, { allLazy = false } = {}) {
|
|
|
137
147
|
for (const [key, value] of Object.entries(routes)) {
|
|
138
148
|
if (typeof value === 'object') {
|
|
139
149
|
result[key] = handleImports(value, routesPath);
|
|
140
|
-
} else if (
|
|
150
|
+
} else if (
|
|
151
|
+
key === 'hooks' ||
|
|
152
|
+
key === 'meta' ||
|
|
153
|
+
(!value.endsWith('.lazy.svelte') && !allLazy)
|
|
154
|
+
) {
|
|
141
155
|
const variableName = pathToCorrectCasing(value);
|
|
142
156
|
importsMap.set(variableName, routesPath + value);
|
|
143
157
|
result[key] = variableName;
|
|
@@ -189,6 +203,7 @@ export function pathToCorrectCasing(value) {
|
|
|
189
203
|
extractLastPart(CATCH_ALL_FILENAME_REGEX) ||
|
|
190
204
|
extractLastPart(PARAM_FILENAME_REGEX) ||
|
|
191
205
|
extractLastPart(HOOKS_FILENAME_REGEX) ||
|
|
206
|
+
extractLastPart(META_FILENAME_REGEX) ||
|
|
192
207
|
extractLastPart(FILENAME_REGEX);
|
|
193
208
|
if (!lastPart) {
|
|
194
209
|
throw new Error(`Invalid filename: ${value}`);
|
|
@@ -196,7 +211,8 @@ export function pathToCorrectCasing(value) {
|
|
|
196
211
|
parts.push(...lastPart.split('-'));
|
|
197
212
|
|
|
198
213
|
const uppercased = parts.map((part, index) => {
|
|
199
|
-
if (index === 0 && lastPart === 'hooks') return part;
|
|
214
|
+
if (index === 0 && (lastPart === 'hooks' || lastPart === 'meta')) return part;
|
|
215
|
+
part = part.replace(/^\[(.*)\]$/, '$1');
|
|
200
216
|
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
201
217
|
});
|
|
202
218
|
return uppercased.join('');
|
package/src/helpers/is-active.js
CHANGED
|
@@ -38,10 +38,12 @@ function compare(compareFn, pathname, params) {
|
|
|
38
38
|
const routeParts = location.pathname.split('/').slice(1);
|
|
39
39
|
for (const [index, pathPart] of pathParts.entries()) {
|
|
40
40
|
const routePart = routeParts[index];
|
|
41
|
-
if (
|
|
41
|
+
if (pathPart.startsWith(':')) {
|
|
42
42
|
continue;
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
if (pathPart !== routePart) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
45
47
|
}
|
|
46
|
-
return
|
|
48
|
+
return true;
|
|
47
49
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* @typedef {import('../index.d.ts').Hooks} Hooks
|
|
7
7
|
*
|
|
8
8
|
* @typedef {import('../index.d.ts').Routes} Routes
|
|
9
|
+
*
|
|
10
|
+
* @typedef {import('../index.d.ts').RouteMeta} RouteMeta
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -15,6 +17,7 @@
|
|
|
15
17
|
* match: RouteComponent | undefined;
|
|
16
18
|
* layouts: LayoutComponent[];
|
|
17
19
|
* hooks: Hooks[];
|
|
20
|
+
* meta: RouteMeta;
|
|
18
21
|
* params: Record<string, string>;
|
|
19
22
|
* breakFromLayouts: boolean;
|
|
20
23
|
* }}
|
|
@@ -39,6 +42,9 @@ export function matchRoute(pathname, routes) {
|
|
|
39
42
|
/** @type {Record<string, string>} */
|
|
40
43
|
let params = {};
|
|
41
44
|
|
|
45
|
+
/** @type {RouteMeta} */
|
|
46
|
+
let meta = {};
|
|
47
|
+
|
|
42
48
|
let breakFromLayouts = false;
|
|
43
49
|
|
|
44
50
|
outer: for (const route of allRoutes) {
|
|
@@ -69,7 +75,7 @@ export function matchRoute(pathname, routes) {
|
|
|
69
75
|
);
|
|
70
76
|
match = /** @type {RouteComponent} */ (routes[resolvedPath]);
|
|
71
77
|
break outer;
|
|
72
|
-
} else if (routePart !== pathPart) {
|
|
78
|
+
} else if (routePart !== pathPart?.toLowerCase()) {
|
|
73
79
|
break;
|
|
74
80
|
}
|
|
75
81
|
|
|
@@ -89,6 +95,10 @@ export function matchRoute(pathname, routes) {
|
|
|
89
95
|
hooks.push(routes.hooks);
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
if ('meta' in routes && routes.meta) {
|
|
99
|
+
meta = { ...meta, ...routes.meta };
|
|
100
|
+
}
|
|
101
|
+
|
|
92
102
|
if (typeof routeMatch === 'function') {
|
|
93
103
|
if (routeParts.length === pathParts.length) {
|
|
94
104
|
match = routeMatch;
|
|
@@ -103,6 +113,7 @@ export function matchRoute(pathname, routes) {
|
|
|
103
113
|
match = result.match;
|
|
104
114
|
params = { ...params, ...result.params };
|
|
105
115
|
hooks.push(...result.hooks);
|
|
116
|
+
meta = { ...meta, ...result.meta };
|
|
106
117
|
if (result.breakFromLayouts) {
|
|
107
118
|
layouts = [];
|
|
108
119
|
} else {
|
|
@@ -113,7 +124,7 @@ export function matchRoute(pathname, routes) {
|
|
|
113
124
|
}
|
|
114
125
|
}
|
|
115
126
|
|
|
116
|
-
return { match, layouts, hooks, params, breakFromLayouts };
|
|
127
|
+
return { match, layouts, hooks, params, meta, breakFromLayouts };
|
|
117
128
|
}
|
|
118
129
|
|
|
119
130
|
/**
|
package/src/helpers/utils.js
CHANGED
|
@@ -25,7 +25,7 @@ export function resolveRouteComponents(input) {
|
|
|
25
25
|
* @param {import('../index.d.ts').RouteComponent} input
|
|
26
26
|
* @returns {Promise<import('svelte').Component>}
|
|
27
27
|
*/
|
|
28
|
-
|
|
28
|
+
function resolveRouteComponent(input) {
|
|
29
29
|
return new Promise((resolve) => {
|
|
30
30
|
if (isLazyImport(input)) {
|
|
31
31
|
Promise.resolve(input()).then((module) => {
|
|
@@ -26,9 +26,12 @@ export function validateRoutes(routes) {
|
|
|
26
26
|
export function getRoutePaths(routes) {
|
|
27
27
|
const paths = [];
|
|
28
28
|
for (const [key, value] of Object.entries(routes)) {
|
|
29
|
+
if (['layout', 'hooks', 'meta'].includes(key)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
29
32
|
if (typeof value === 'object') {
|
|
30
33
|
paths.push(
|
|
31
|
-
...getRoutePaths(value).map((path) => {
|
|
34
|
+
...getRoutePaths(/** @type {import('../index.d.ts').Routes} */ (value)).map((path) => {
|
|
32
35
|
if (path === '*') {
|
|
33
36
|
return key + '/*';
|
|
34
37
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export type Routes = {
|
|
|
74
74
|
[_: `*${string}` | `(*${string})`]: RouteComponent | undefined;
|
|
75
75
|
layout?: LayoutComponent;
|
|
76
76
|
hooks?: Hooks;
|
|
77
|
+
meta?: RouteMeta;
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
export type IsActiveLink = Action<
|
|
@@ -81,6 +82,20 @@ export type IsActiveLink = Action<
|
|
|
81
82
|
{ className?: string; startsWith?: boolean } | undefined
|
|
82
83
|
>;
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Route metadata that can be extended via module augmentation.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* declare module 'sv-router' {
|
|
90
|
+
* interface RouteMeta {
|
|
91
|
+
* public?: boolean;
|
|
92
|
+
* requiresAuth?: boolean;
|
|
93
|
+
* }
|
|
94
|
+
* }
|
|
95
|
+
*/
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
97
|
+
export interface RouteMeta {}
|
|
98
|
+
|
|
84
99
|
export type RouterApi<T extends Routes> = {
|
|
85
100
|
/**
|
|
86
101
|
* Construct a path while ensuring type safety.
|
|
@@ -147,6 +162,17 @@ export type RouterApi<T extends Routes> = {
|
|
|
147
162
|
* 'hello-world', commentId: '123' }`.
|
|
148
163
|
*/
|
|
149
164
|
params: AllParams<T>;
|
|
165
|
+
/**
|
|
166
|
+
* Extract parameters from the given pathname. Will throw if the pathname does not match the
|
|
167
|
+
* current route.
|
|
168
|
+
*
|
|
169
|
+
* ```ts
|
|
170
|
+
* route.getParams('/posts/:slug').slug;
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* @param pathname
|
|
174
|
+
*/
|
|
175
|
+
getParams<U extends Path<T>>(pathname: U): Record<ExtractParams<U>, string>;
|
|
150
176
|
/** The reactive pathname of the URL. */
|
|
151
177
|
pathname: (Path<T, true> & {}) | (string & {});
|
|
152
178
|
/** The reactive query string part of the URL. */
|
|
@@ -155,6 +181,8 @@ export type RouterApi<T extends Routes> = {
|
|
|
155
181
|
state: unknown;
|
|
156
182
|
/** The reactive hash part of the URL. */
|
|
157
183
|
hash: string;
|
|
184
|
+
/** Arbitrary metadata associated with the route. */
|
|
185
|
+
meta: RouteMeta;
|
|
158
186
|
};
|
|
159
187
|
};
|
|
160
188
|
|
|
@@ -210,7 +238,9 @@ type StripNonRoutes<T extends Routes> = {
|
|
|
210
238
|
? never
|
|
211
239
|
: K extends 'hooks'
|
|
212
240
|
? never
|
|
213
|
-
: K
|
|
241
|
+
: K extends 'meta'
|
|
242
|
+
? never
|
|
243
|
+
: K]: T[K] extends Routes ? StripNonRoutes<T[K]> : T[K];
|
|
214
244
|
};
|
|
215
245
|
|
|
216
246
|
type RecursiveKeys<
|