svelte-navigator-lite 1.1.4 → 2.0.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 +116 -218
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -2
- package/dist/match.d.ts +16 -0
- package/dist/match.js +54 -0
- package/dist/match.test.d.ts +1 -0
- package/dist/match.test.js +120 -0
- package/dist/router.svelte.d.ts +16 -20
- package/dist/router.svelte.js +66 -117
- package/dist/router.test.js +170 -253
- package/dist/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/package.json +9 -18
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# svelte-navigator-lite
|
|
2
2
|
|
|
3
|
-
A lightweight,
|
|
3
|
+
A lightweight, pattern-based router for Svelte 5. Routes are defined with familiar URL patterns, guards are reusable named functions, and the reactive `router` singleton integrates directly with Svelte runes.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,261 +8,159 @@ A lightweight, label-based router for Svelte 5. Define named routes that map to
|
|
|
8
8
|
npm install svelte-navigator-lite
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick start
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
import { createRouter, router } from 'svelte-navigator-lite';
|
|
17
|
-
import type { RouteList } from 'svelte-navigator-lite';
|
|
18
|
-
|
|
19
|
-
const routes: RouteList = {
|
|
20
|
-
'login': {
|
|
21
|
-
rootPath: 'login',
|
|
22
|
-
segments: [],
|
|
23
|
-
},
|
|
24
|
-
'dashboard': {
|
|
25
|
-
rootPath: 'dashboard',
|
|
26
|
-
segments: [],
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
createRouter(routes, 'dashboard'); // second argument is the default/fallback route
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Then use `router.route` anywhere in your Svelte components — it's a reactive Svelte 5 `$state` value:
|
|
13
|
+
Define your routes and guards, then call `createRouter` on mount.
|
|
34
14
|
|
|
35
15
|
```svelte
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
{/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Mixed segments
|
|
82
|
-
|
|
83
|
-
`enforceVal` and `name` segments can be combined in any order:
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
'edit-event': {
|
|
87
|
-
rootPath: 'event',
|
|
88
|
-
segments: [
|
|
89
|
-
{ name: 'eventId' }, // matches /event/:eventId/edit
|
|
90
|
-
{ enforceVal: 'edit' },
|
|
91
|
-
],
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Optional segments
|
|
96
|
-
|
|
97
|
-
Add `optional: true` to make a trailing segment optional. Optional segments must come after all required segments.
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
'event': {
|
|
101
|
-
rootPath: 'event',
|
|
102
|
-
segments: [
|
|
103
|
-
{ name: 'eventId' },
|
|
104
|
-
{ enforceVal: 'edit', optional: true }, // matches /event/123 AND /event/123/edit
|
|
105
|
-
],
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## Search Params
|
|
112
|
-
|
|
113
|
-
Search params are automatically captured into `router.searchParams` when present in the URL. No configuration is needed — they never affect whether a route matches.
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
// /password-reset?token=abc → router.searchParams.token === 'abc'
|
|
117
|
-
// /password-reset → router.searchParams === {}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
---
|
|
16
|
+
<!-- App.svelte -->
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
import { onMount } from 'svelte';
|
|
19
|
+
import { createRouter, router } from 'svelte-navigator-lite';
|
|
20
|
+
import { auth } from '$stores/auth.svelte';
|
|
21
|
+
import AppShell from '$lib/components/AppShell.svelte';
|
|
22
|
+
|
|
23
|
+
onMount(() => {
|
|
24
|
+
createRouter({
|
|
25
|
+
routes: [
|
|
26
|
+
{ name: 'cal', pattern: '/cal' },
|
|
27
|
+
{ name: 'cal-date', pattern: '/cal/:mm/:dd/:yyyy' },
|
|
28
|
+
{ name: 'event', pattern: '/event/:eventId' },
|
|
29
|
+
{ name: 'edit', pattern: '/event/:eventId/edit' },
|
|
30
|
+
{ name: 'login', pattern: '/login', guards: ['unauth'] },
|
|
31
|
+
{ name: 'signup', pattern: '/signup', guards: ['unauth'] },
|
|
32
|
+
],
|
|
33
|
+
guards: {
|
|
34
|
+
auth: { condition: () => !auth.isValid(), redirectTo: 'login' },
|
|
35
|
+
unauth: { condition: () => auth.isValid(), redirectTo: 'cal' },
|
|
36
|
+
},
|
|
37
|
+
fallback: 'cal',
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<AppShell />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Route patterns
|
|
46
|
+
|
|
47
|
+
Patterns follow standard URL conventions.
|
|
48
|
+
|
|
49
|
+
| Pattern | Example URL | Params |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `/cal` | `/cal` | `{}` |
|
|
52
|
+
| `/event/:eventId` | `/event/abc-123` | `{ eventId: 'abc-123' }` |
|
|
53
|
+
| `/event/:eventId/edit` | `/event/abc-123/edit` | `{ eventId: 'abc-123' }` |
|
|
54
|
+
| `/cal/:mm/:dd/:yyyy` | `/cal/05/13/2026` | `{ mm: '05', dd: '13', yyyy: '2026' }` |
|
|
55
|
+
| `/settings/:page?` | `/settings` or `/settings/account` | `{}` or `{ page: 'account' }` |
|
|
56
|
+
|
|
57
|
+
Append `?` to a param name to make it optional. Optional params must come at the end of the pattern.
|
|
121
58
|
|
|
122
59
|
## Navigating
|
|
123
60
|
|
|
124
|
-
|
|
61
|
+
```typescript
|
|
62
|
+
import { router } from 'svelte-navigator-lite';
|
|
125
63
|
|
|
126
|
-
|
|
64
|
+
// Static route
|
|
65
|
+
router.navigate('login');
|
|
127
66
|
|
|
128
|
-
|
|
67
|
+
// With path params
|
|
129
68
|
router.navigate('event', { eventId: '123' });
|
|
130
|
-
// → /event/123
|
|
131
69
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
router.navigate('event', { eventId: '123' }, { tab: 'details' });
|
|
136
|
-
// → /event/123?tab=details
|
|
70
|
+
// With path params + search params
|
|
71
|
+
router.navigate('edit', { eventId: '123' }, { from: 'calendar' });
|
|
137
72
|
```
|
|
138
73
|
|
|
139
|
-
|
|
74
|
+
`navigate` returns a `Promise<void>`. Guards are evaluated before navigation — if a guard fires, the router redirects instead and original params are not forwarded.
|
|
140
75
|
|
|
141
|
-
|
|
76
|
+
## Reading state
|
|
142
77
|
|
|
143
|
-
|
|
78
|
+
```typescript
|
|
79
|
+
import { router } from 'svelte-navigator-lite';
|
|
144
80
|
|
|
145
|
-
|
|
146
|
-
|
|
81
|
+
router.route // current route name: string
|
|
82
|
+
router.params // path params: Record<string, string>
|
|
83
|
+
router.searchParams // query params: Record<string, string>
|
|
84
|
+
router.notFound // true if the URL matched no route and the fallback was used
|
|
147
85
|
|
|
148
|
-
|
|
149
|
-
|
|
86
|
+
router.is('cal') // true if current route === 'cal'
|
|
87
|
+
router.matches(['cal', 'event']) // true if current route is in the list
|
|
150
88
|
```
|
|
151
89
|
|
|
152
|
-
|
|
90
|
+
All properties are reactive — use them in Svelte templates or `$effect` blocks and they update automatically on navigation.
|
|
153
91
|
|
|
154
|
-
##
|
|
92
|
+
## Guards
|
|
155
93
|
|
|
156
|
-
Guards
|
|
94
|
+
Guards are defined once in `createRouter` and referenced by name in route definitions.
|
|
157
95
|
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
rootPath: 'dashboard',
|
|
180
|
-
segments: [],
|
|
181
|
-
routeGuards: [guards.authenticated],
|
|
96
|
+
```typescript
|
|
97
|
+
createRouter({
|
|
98
|
+
routes: [
|
|
99
|
+
{ name: 'cal', pattern: '/cal', guards: ['auth'] },
|
|
100
|
+
{ name: 'login', pattern: '/login', guards: ['unauth'] },
|
|
101
|
+
{ name: 'verify-email', pattern: '/verify-email', guards: ['unauth'] },
|
|
102
|
+
{ name: 'login-check', pattern: '/login', guards: ['unauth', 'emailCheck'] },
|
|
103
|
+
],
|
|
104
|
+
guards: {
|
|
105
|
+
auth: {
|
|
106
|
+
condition: () => !auth.isValid(),
|
|
107
|
+
redirectTo: 'login',
|
|
108
|
+
},
|
|
109
|
+
unauth: {
|
|
110
|
+
condition: () => auth.isValid(),
|
|
111
|
+
redirectTo: 'cal',
|
|
112
|
+
},
|
|
113
|
+
emailCheck: {
|
|
114
|
+
condition: () => auth.containsCreds() && !auth.user?.verified,
|
|
115
|
+
redirectTo: 'verify-email',
|
|
116
|
+
},
|
|
182
117
|
},
|
|
183
|
-
|
|
118
|
+
fallback: 'cal',
|
|
119
|
+
});
|
|
184
120
|
```
|
|
185
121
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## API Reference
|
|
191
|
-
|
|
192
|
-
### `createRouter(routes, defaultRoute)`
|
|
193
|
-
|
|
194
|
-
Registers all routes and sets the fallback route. Parses the current URL immediately. Must be called once before using `router`.
|
|
195
|
-
|
|
196
|
-
### `router.route`
|
|
122
|
+
**`condition`** — called before entering the route. Return `true` to trigger the redirect.
|
|
197
123
|
|
|
198
|
-
|
|
124
|
+
**`redirectTo`** — route name to navigate to instead.
|
|
199
125
|
|
|
200
|
-
|
|
126
|
+
Multiple guards on a route are checked in order; the first one that fires wins. Guard redirects are also checked for guards (recursively), with cycle detection to prevent infinite loops.
|
|
201
127
|
|
|
202
|
-
|
|
128
|
+
## `page` store
|
|
203
129
|
|
|
204
|
-
|
|
130
|
+
A Svelte readable store that emits `{ url: URL }` on every navigation. Compatible with SvelteKit's `$app/stores` page store shape.
|
|
205
131
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
### `router.notFound`
|
|
209
|
-
|
|
210
|
-
`true` when the current URL did not match any defined route and the router fell back to `defaultRoute`. Use this to render a 404 state without needing a dedicated catch-all route.
|
|
132
|
+
```typescript
|
|
133
|
+
import { page } from 'svelte-navigator-lite';
|
|
211
134
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
{:else if router.is('dashboard')}
|
|
216
|
-
<Dashboard />
|
|
217
|
-
{/if}
|
|
135
|
+
page.subscribe(({ url }) => {
|
|
136
|
+
console.log(url.pathname);
|
|
137
|
+
});
|
|
218
138
|
```
|
|
219
139
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Navigate to a named route, applying guards and building the URL from the route definition.
|
|
223
|
-
|
|
224
|
-
### `router.is(route)`
|
|
225
|
-
|
|
226
|
-
Returns `true` if `router.route === route`. Useful for active link styling.
|
|
227
|
-
|
|
228
|
-
```ts
|
|
229
|
-
class:active={router.is('dashboard')}
|
|
230
|
-
```
|
|
140
|
+
## `goto`
|
|
231
141
|
|
|
232
|
-
|
|
142
|
+
Low-level path navigation. Prefer `router.navigate` for named routes.
|
|
233
143
|
|
|
234
|
-
|
|
144
|
+
```typescript
|
|
145
|
+
import { goto } from 'svelte-navigator-lite';
|
|
235
146
|
|
|
236
|
-
|
|
237
|
-
|
|
147
|
+
await goto('/some/path');
|
|
148
|
+
await goto('/some/path', { replaceState: true }); // replace instead of push
|
|
238
149
|
```
|
|
239
150
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
Register a new route.
|
|
243
|
-
```ts
|
|
244
|
-
registerRoute('settings', {
|
|
245
|
-
rootPath: 'settings',
|
|
246
|
-
segments: [],
|
|
247
|
-
routeGuards: [guards.authenticated],
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
### `page`
|
|
151
|
+
## Migrating from v1
|
|
251
152
|
|
|
252
|
-
|
|
153
|
+
The `rootPath` + `segments` route definition is replaced by a single `pattern` string, and `routeGuards` inline on each route are replaced by named guards defined once.
|
|
253
154
|
|
|
254
|
-
```
|
|
255
|
-
|
|
155
|
+
```typescript
|
|
156
|
+
// v1
|
|
157
|
+
'edit': {
|
|
158
|
+
rootPath: 'event',
|
|
159
|
+
segments: [{ name: 'eventId' }, { enforceVal: 'edit' }],
|
|
160
|
+
routeGuards: [{ fn: () => !auth.isValid(), redirectTo: 'login' }],
|
|
161
|
+
}
|
|
256
162
|
|
|
257
|
-
|
|
163
|
+
// v2
|
|
164
|
+
{ name: 'edit', pattern: '/event/:eventId/edit', guards: ['auth'] }
|
|
165
|
+
// with guard defined once in createRouter: auth: { condition: () => !auth.isValid(), redirectTo: 'login' }
|
|
258
166
|
```
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## Matching Rules
|
|
263
|
-
|
|
264
|
-
- Routes are matched in the order they are defined — first match wins.
|
|
265
|
-
- `rootPath` is always required and must match the first URL segment exactly.
|
|
266
|
-
- Segment count must be between `required segments + 1` and `total segments + 1` (the `+1` accounts for `rootPath`).
|
|
267
|
-
- A route with no segments only matches its `rootPath` with nothing after it (e.g. `/login` not `/login/extra`).
|
|
268
|
-
- Unmatched URLs fall back to the `defaultRoute`.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createRouter, router, page, goto } from './router.svelte';
|
|
2
|
-
export type {
|
|
1
|
+
export { createRouter, router, page, goto, _createRouter } from './router.svelte.js';
|
|
2
|
+
export type { RouteDefinition, Guard, RouterConfig } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createRouter, router, page, goto } from './router.svelte';
|
|
1
|
+
export { createRouter, router, page, goto, _createRouter } from './router.svelte.js';
|
package/dist/match.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RouteDefinition } from './types.js';
|
|
2
|
+
type CompiledRoute = {
|
|
3
|
+
name: string;
|
|
4
|
+
pattern: string;
|
|
5
|
+
regex: RegExp;
|
|
6
|
+
paramNames: string[];
|
|
7
|
+
guards: string[];
|
|
8
|
+
};
|
|
9
|
+
export type RouteMatch = {
|
|
10
|
+
name: string;
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
export declare function compileRoutes(routes: RouteDefinition[]): CompiledRoute[];
|
|
14
|
+
export declare function matchPath(compiled: CompiledRoute[], pathname: string): RouteMatch | null;
|
|
15
|
+
export declare function buildPath(pattern: string, params?: Record<string, string>): string;
|
|
16
|
+
export {};
|
package/dist/match.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function compileRoutes(routes) {
|
|
2
|
+
return routes.map(route => {
|
|
3
|
+
const paramNames = [];
|
|
4
|
+
const regexStr = route.pattern
|
|
5
|
+
// Escape regex special chars (except : and /)
|
|
6
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
7
|
+
// Optional params /:name? — make the whole slash+segment optional together
|
|
8
|
+
.replace(/\/:([a-zA-Z_][a-zA-Z0-9_]*)\?/g, (_, name) => {
|
|
9
|
+
paramNames.push(name);
|
|
10
|
+
return '(?:/([^/]*))?';
|
|
11
|
+
})
|
|
12
|
+
// Required params :name
|
|
13
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
14
|
+
paramNames.push(name);
|
|
15
|
+
return '([^/]+)';
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
name: route.name,
|
|
19
|
+
pattern: route.pattern,
|
|
20
|
+
regex: new RegExp(`^${regexStr}/?$`),
|
|
21
|
+
paramNames,
|
|
22
|
+
guards: route.guards ?? [],
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function matchPath(compiled, pathname) {
|
|
27
|
+
for (const route of compiled) {
|
|
28
|
+
const m = pathname.match(route.regex);
|
|
29
|
+
if (!m)
|
|
30
|
+
continue;
|
|
31
|
+
const params = {};
|
|
32
|
+
route.paramNames.forEach((name, i) => {
|
|
33
|
+
const val = m[i + 1];
|
|
34
|
+
if (val)
|
|
35
|
+
params[name] = decodeURIComponent(val);
|
|
36
|
+
});
|
|
37
|
+
return { name: route.name, params };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
// Build a URL path from a pattern and params.
|
|
42
|
+
// Optional params omitted from params are dropped cleanly.
|
|
43
|
+
export function buildPath(pattern, params = {}) {
|
|
44
|
+
const path = pattern
|
|
45
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)\?/g, (_, name) => name in params ? encodeURIComponent(params[name]) : '')
|
|
46
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
47
|
+
if (!(name in params))
|
|
48
|
+
throw new Error(`[router] missing required param "${name}" for pattern "${pattern}"`);
|
|
49
|
+
return encodeURIComponent(params[name]);
|
|
50
|
+
})
|
|
51
|
+
.replace(/\/+/g, '/') // collapse double slashes left by dropped optional params
|
|
52
|
+
.replace(/\/$/, ''); // strip trailing slash
|
|
53
|
+
return path || '/';
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { compileRoutes, matchPath, buildPath } from './match';
|
|
3
|
+
// ─── matchPath ────────────────────────────────────────────────────────────────
|
|
4
|
+
describe('matchPath — static routes', () => {
|
|
5
|
+
it('matches an exact path', () => {
|
|
6
|
+
const compiled = compileRoutes([{ name: 'login', pattern: '/login' }]);
|
|
7
|
+
expect(matchPath(compiled, '/login')).toEqual({ name: 'login', params: {} });
|
|
8
|
+
});
|
|
9
|
+
it('does not match a different path', () => {
|
|
10
|
+
const compiled = compileRoutes([{ name: 'login', pattern: '/login' }]);
|
|
11
|
+
expect(matchPath(compiled, '/signup')).toBeNull();
|
|
12
|
+
});
|
|
13
|
+
it('returns null when no routes match', () => {
|
|
14
|
+
const compiled = compileRoutes([{ name: 'login', pattern: '/login' }]);
|
|
15
|
+
expect(matchPath(compiled, '/unknown/path')).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
it('returns the first matching route', () => {
|
|
18
|
+
const compiled = compileRoutes([
|
|
19
|
+
{ name: 'first', pattern: '/cal' },
|
|
20
|
+
{ name: 'second', pattern: '/cal' },
|
|
21
|
+
]);
|
|
22
|
+
expect(matchPath(compiled, '/cal')?.name).toBe('first');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('matchPath — required params', () => {
|
|
26
|
+
it('captures a single param', () => {
|
|
27
|
+
const compiled = compileRoutes([{ name: 'event', pattern: '/event/:eventId' }]);
|
|
28
|
+
expect(matchPath(compiled, '/event/abc-123')).toEqual({
|
|
29
|
+
name: 'event',
|
|
30
|
+
params: { eventId: 'abc-123' },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it('does not match when a required param is absent', () => {
|
|
34
|
+
const compiled = compileRoutes([{ name: 'event', pattern: '/event/:eventId' }]);
|
|
35
|
+
expect(matchPath(compiled, '/event')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it('captures multiple params', () => {
|
|
38
|
+
const compiled = compileRoutes([{ name: 'cal-date', pattern: '/cal/:mm/:dd/:yyyy' }]);
|
|
39
|
+
expect(matchPath(compiled, '/cal/05/13/2026')).toEqual({
|
|
40
|
+
name: 'cal-date',
|
|
41
|
+
params: { mm: '05', dd: '13', yyyy: '2026' },
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it('captures params mixed with literal segments', () => {
|
|
45
|
+
const compiled = compileRoutes([{ name: 'edit', pattern: '/event/:eventId/edit' }]);
|
|
46
|
+
expect(matchPath(compiled, '/event/99/edit')).toEqual({
|
|
47
|
+
name: 'edit',
|
|
48
|
+
params: { eventId: '99' },
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it('does not match when a literal segment differs', () => {
|
|
52
|
+
const compiled = compileRoutes([{ name: 'edit', pattern: '/event/:eventId/edit' }]);
|
|
53
|
+
expect(matchPath(compiled, '/event/99/delete')).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
it('does not match when there are extra segments', () => {
|
|
56
|
+
const compiled = compileRoutes([{ name: 'event', pattern: '/event/:eventId' }]);
|
|
57
|
+
expect(matchPath(compiled, '/event/99/unexpected')).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
it('differentiates routes with the same prefix by segment count', () => {
|
|
60
|
+
const compiled = compileRoutes([
|
|
61
|
+
{ name: 'event', pattern: '/event/:eventId' },
|
|
62
|
+
{ name: 'edit', pattern: '/event/:eventId/edit' },
|
|
63
|
+
]);
|
|
64
|
+
expect(matchPath(compiled, '/event/99')?.name).toBe('event');
|
|
65
|
+
expect(matchPath(compiled, '/event/99/edit')?.name).toBe('edit');
|
|
66
|
+
});
|
|
67
|
+
it('URL-decodes param values', () => {
|
|
68
|
+
const compiled = compileRoutes([{ name: 'search', pattern: '/search/:query' }]);
|
|
69
|
+
expect(matchPath(compiled, '/search/hello%20world')?.params.query).toBe('hello world');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('matchPath — optional params', () => {
|
|
73
|
+
const compiled = compileRoutes([{ name: 'settings', pattern: '/settings/:page?' }]);
|
|
74
|
+
it('matches when the optional param is present', () => {
|
|
75
|
+
expect(matchPath(compiled, '/settings/account')).toEqual({
|
|
76
|
+
name: 'settings',
|
|
77
|
+
params: { page: 'account' },
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
it('matches when the optional param is absent', () => {
|
|
81
|
+
const result = matchPath(compiled, '/settings');
|
|
82
|
+
expect(result?.name).toBe('settings');
|
|
83
|
+
expect(result?.params).not.toHaveProperty('page');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
// ─── buildPath ────────────────────────────────────────────────────────────────
|
|
87
|
+
describe('buildPath — static patterns', () => {
|
|
88
|
+
it('returns the pattern as-is', () => {
|
|
89
|
+
expect(buildPath('/login')).toBe('/login');
|
|
90
|
+
});
|
|
91
|
+
it('returns / for an empty pattern', () => {
|
|
92
|
+
expect(buildPath('')).toBe('/');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('buildPath — required params', () => {
|
|
96
|
+
it('substitutes a single param', () => {
|
|
97
|
+
expect(buildPath('/event/:eventId', { eventId: '42' })).toBe('/event/42');
|
|
98
|
+
});
|
|
99
|
+
it('substitutes multiple params', () => {
|
|
100
|
+
expect(buildPath('/cal/:mm/:dd/:yyyy', { mm: '05', dd: '13', yyyy: '2026' }))
|
|
101
|
+
.toBe('/cal/05/13/2026');
|
|
102
|
+
});
|
|
103
|
+
it('substitutes params among literal segments', () => {
|
|
104
|
+
expect(buildPath('/event/:eventId/edit', { eventId: '99' })).toBe('/event/99/edit');
|
|
105
|
+
});
|
|
106
|
+
it('URL-encodes param values', () => {
|
|
107
|
+
expect(buildPath('/search/:query', { query: 'hello world' })).toBe('/search/hello%20world');
|
|
108
|
+
});
|
|
109
|
+
it('throws when a required param is missing', () => {
|
|
110
|
+
expect(() => buildPath('/event/:eventId', {})).toThrow('missing required param "eventId"');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('buildPath — optional params', () => {
|
|
114
|
+
it('inserts the value when the optional param is provided', () => {
|
|
115
|
+
expect(buildPath('/settings/:page?', { page: 'account' })).toBe('/settings/account');
|
|
116
|
+
});
|
|
117
|
+
it('drops the segment cleanly when the optional param is absent', () => {
|
|
118
|
+
expect(buildPath('/settings/:page?', {})).toBe('/settings');
|
|
119
|
+
});
|
|
120
|
+
});
|
package/dist/router.svelte.d.ts
CHANGED
|
@@ -1,35 +1,31 @@
|
|
|
1
|
+
import type { RouterConfig } from './types.js';
|
|
1
2
|
export declare const page: import("svelte/store").Readable<{
|
|
2
3
|
url: URL;
|
|
3
4
|
}>;
|
|
4
5
|
export declare function goto(path: string, { replaceState }?: {
|
|
5
6
|
replaceState?: boolean | undefined;
|
|
6
7
|
}): Promise<void>;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
fn: () => boolean;
|
|
18
|
-
redirectTo: string;
|
|
8
|
+
declare function _createRouter(): {
|
|
9
|
+
readonly route: string;
|
|
10
|
+
readonly params: Record<string, string>;
|
|
11
|
+
readonly searchParams: Record<string, string>;
|
|
12
|
+
readonly notFound: boolean;
|
|
13
|
+
is(route: string): boolean;
|
|
14
|
+
matches(routes: string[]): boolean;
|
|
15
|
+
parseUrl: (url: string) => void;
|
|
16
|
+
navigate(routeName: string, params?: Record<string, string>, searchParams?: Record<string, string>): Promise<void>;
|
|
17
|
+
init(config: RouterConfig): void;
|
|
19
18
|
};
|
|
20
|
-
export
|
|
21
|
-
export
|
|
22
|
-
export declare function _createRouter(routeList?: RouteList): {
|
|
19
|
+
export { _createRouter };
|
|
20
|
+
export declare const router: {
|
|
23
21
|
readonly route: string;
|
|
24
22
|
readonly params: Record<string, string>;
|
|
25
23
|
readonly searchParams: Record<string, string>;
|
|
26
24
|
readonly notFound: boolean;
|
|
27
|
-
rootRoute: string;
|
|
28
25
|
is(route: string): boolean;
|
|
29
26
|
matches(routes: string[]): boolean;
|
|
30
27
|
parseUrl: (url: string) => void;
|
|
31
|
-
navigate(
|
|
32
|
-
|
|
28
|
+
navigate(routeName: string, params?: Record<string, string>, searchParams?: Record<string, string>): Promise<void>;
|
|
29
|
+
init(config: RouterConfig): void;
|
|
33
30
|
};
|
|
34
|
-
export declare
|
|
35
|
-
export declare function createRouter(RouteList: RouteList, defaultRoute: string): void;
|
|
31
|
+
export declare function createRouter(config: RouterConfig): void;
|