svelte-navigator-lite 1.1.2 → 1.1.4

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 CHANGED
@@ -110,32 +110,30 @@ Add `optional: true` to make a trailing segment optional. Optional segments must
110
110
 
111
111
  ## Search Params
112
112
 
113
- Declared in `searchParams`. Listed params are captured into `router.params` if present in the URL, and silently ignored if absent. They never affect whether a route matches.
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
114
 
115
115
  ```ts
116
- 'password-reset': {
117
- rootPath: 'password-reset',
118
- segments: [],
119
- searchParams: ['token'],
120
- }
121
- // /password-reset?token=abc → router.params.token === 'abc'
122
- // /password-reset → router.params === {}
116
+ // /password-reset?token=abc → router.searchParams.token === 'abc'
117
+ // /password-reset → router.searchParams === {}
123
118
  ```
124
119
 
125
120
  ---
126
121
 
127
122
  ## Navigating
128
123
 
129
- ### `router.navigate(route, params?)`
124
+ ### `router.navigate(route, params?, searchParams?)`
130
125
 
131
- Navigate to a named route. Params are used to fill dynamic segments and search params.
126
+ Navigate to a named route. Path params fill dynamic segments; pass search params separately as the third argument.
132
127
 
133
128
  ```ts
134
129
  router.navigate('event', { eventId: '123' });
135
130
  // → /event/123
136
131
 
137
- router.navigate('password-reset', { token: 'abc123' });
132
+ router.navigate('password-reset', undefined, { token: 'abc123' });
138
133
  // → /password-reset?token=abc123
134
+
135
+ router.navigate('event', { eventId: '123' }, { tab: 'details' });
136
+ // → /event/123?tab=details
139
137
  ```
140
138
 
141
139
  Throws if a required param is missing.
@@ -201,7 +199,11 @@ The label of the currently matched route. Falls back to `defaultRoute` if no rou
201
199
 
202
200
  ### `router.params`
203
201
 
204
- An object containing all captured values — path params, required search params, and any present optional search params.
202
+ An object containing the captured path param values for the current route.
203
+
204
+ ### `router.searchParams`
205
+
206
+ An object containing the search params present in the current URL. Empty object when none are present.
205
207
 
206
208
  ### `router.notFound`
207
209
 
@@ -215,7 +217,7 @@ An object containing all captured values — path params, required search params
215
217
  {/if}
216
218
  ```
217
219
 
218
- ### `router.navigate(route, params?)`
220
+ ### `router.navigate(route, params?, searchParams?)`
219
221
 
220
222
  Navigate to a named route, applying guards and building the URL from the route definition.
221
223
 
@@ -11,8 +11,6 @@ export type Route = {
11
11
  enforceVal?: string;
12
12
  optional?: boolean;
13
13
  })[];
14
- searchParams?: string[];
15
- optionalSearchParams?: string[];
16
14
  routeGuards?: RouteGuard[];
17
15
  };
18
16
  export type RouteGuard = {
@@ -24,12 +22,13 @@ export type Router = ReturnType<typeof _createRouter>;
24
22
  export declare function _createRouter(routeList?: RouteList): {
25
23
  readonly route: string;
26
24
  readonly params: Record<string, string>;
25
+ readonly searchParams: Record<string, string>;
27
26
  readonly notFound: boolean;
28
27
  rootRoute: string;
29
28
  is(route: string): boolean;
30
29
  matches(routes: string[]): boolean;
31
30
  parseUrl: (url: string) => void;
32
- navigate(route: string, params?: Record<string, string>): Promise<void>;
31
+ navigate(route: string, params?: Record<string, string>, searchParams?: Record<string, string>): Promise<void>;
33
32
  registerRoute(name: string, route: Route): void;
34
33
  };
35
34
  export declare let router: Router;
@@ -38,6 +38,7 @@ export function _createRouter(routeList = {}) {
38
38
  let state = $state({
39
39
  current: '',
40
40
  params: {},
41
+ searchParams: {},
41
42
  notFound: false,
42
43
  });
43
44
  let routes = {};
@@ -75,32 +76,17 @@ export function _createRouter(routeList = {}) {
75
76
  params[routeSegment.name] = urlSegment;
76
77
  }
77
78
  }
78
- if (match && route.searchParams) {
79
- for (const key of route.searchParams) {
80
- const val = searchParams.get(key);
81
- if (val === null) {
82
- match = false;
83
- break;
84
- }
85
- params[key] = val;
86
- }
87
- }
88
- if (match && route.optionalSearchParams) {
89
- for (const key of route.optionalSearchParams) {
90
- const val = searchParams.get(key);
91
- if (val !== null)
92
- params[key] = val;
93
- }
94
- }
95
79
  if (match) {
96
80
  state.current = routeName;
97
81
  state.params = params;
82
+ state.searchParams = Object.fromEntries(searchParams.entries());
98
83
  state.notFound = false;
99
84
  return;
100
85
  }
101
86
  }
102
87
  state.current = rootRoute;
103
88
  state.params = {};
89
+ state.searchParams = {};
104
90
  state.notFound = true;
105
91
  }
106
92
  return {
@@ -111,6 +97,9 @@ export function _createRouter(routeList = {}) {
111
97
  get params() {
112
98
  return state.params;
113
99
  },
100
+ get searchParams() {
101
+ return state.searchParams;
102
+ },
114
103
  get notFound() {
115
104
  return state.notFound;
116
105
  },
@@ -124,7 +113,7 @@ export function _createRouter(routeList = {}) {
124
113
  return routes.includes(state.current);
125
114
  },
126
115
  parseUrl,
127
- async navigate(route, params) {
116
+ async navigate(route, params, searchParams) {
128
117
  let path = routes[route].rootPath;
129
118
  for (const guard of routes[route].routeGuards || []) {
130
119
  if (guard.fn()) {
@@ -143,14 +132,10 @@ export function _createRouter(routeList = {}) {
143
132
  path += `/${params[segment.name]}`;
144
133
  }
145
134
  }
146
- if (routes[route].searchParams) {
147
- const searchParams = new URLSearchParams();
148
- for (const param of routes[route].searchParams) {
149
- if (params && params[param]) {
150
- searchParams.set(param, params[param]);
151
- }
152
- }
153
- path += `?${searchParams.toString()}`;
135
+ if (searchParams) {
136
+ const qs = new URLSearchParams(searchParams).toString();
137
+ if (qs)
138
+ path += `?${qs}`;
154
139
  }
155
140
  await goto(path);
156
141
  },
@@ -129,46 +129,40 @@ describe('parseUrl — optional segments', () => {
129
129
  expect(r.notFound).toBe(true);
130
130
  });
131
131
  });
132
- describe('parseUrl — required searchParams', () => {
132
+ describe('parseUrl — search params', () => {
133
133
  const routes = {
134
134
  'password-reset': {
135
135
  rootPath: 'password-reset',
136
136
  segments: [],
137
- searchParams: ['token'],
138
137
  },
139
138
  fallback: { rootPath: 'fallback', segments: [] },
140
139
  };
141
- it('matches and captures a required search param', () => {
140
+ it('captures a search param when present', () => {
142
141
  const r = makeRouter(routes, 'fallback');
143
142
  r.parseUrl('http://localhost/password-reset?token=abc123');
144
143
  expect(r.route).toBe('password-reset');
145
- expect(r.params).toEqual({ token: 'abc123' });
144
+ expect(r.searchParams).toEqual({ token: 'abc123' });
146
145
  });
147
- it('does not match when a required search param is absent', () => {
146
+ it('still matches when no search params are present', () => {
148
147
  const r = makeRouter(routes, 'fallback');
149
148
  r.parseUrl('http://localhost/password-reset');
150
- expect(r.notFound).toBe(true);
149
+ expect(r.route).toBe('password-reset');
150
+ expect(r.searchParams).toEqual({});
151
151
  });
152
- });
153
- describe('parseUrl optional searchParams', () => {
154
- const routes = {
155
- signup: {
156
- rootPath: 'signup',
157
- segments: [],
158
- optionalSearchParams: ['redirect'],
159
- },
160
- };
161
- it('captures optional search param when present', () => {
162
- const r = makeRouter(routes, 'signup');
163
- r.parseUrl('http://localhost/signup?redirect=%2Fdashboard');
164
- expect(r.route).toBe('signup');
165
- expect(r.params).toEqual({ redirect: '/dashboard' });
166
- });
167
- it('still matches when optional search param is absent', () => {
168
- const r = makeRouter(routes, 'signup');
169
- r.parseUrl('http://localhost/signup');
170
- expect(r.route).toBe('signup');
171
- expect(r.params).toEqual({});
152
+ it('captures multiple search params', () => {
153
+ const r = makeRouter({
154
+ items: { rootPath: 'items', segments: [] },
155
+ }, 'items');
156
+ r.parseUrl('http://localhost/items?page=2&sort=asc');
157
+ expect(r.searchParams).toEqual({ page: '2', sort: 'asc' });
158
+ });
159
+ it('captures search params alongside path params', () => {
160
+ const r = makeRouter({
161
+ event: { rootPath: 'event', segments: [{ name: 'eventId' }] },
162
+ }, 'event');
163
+ r.parseUrl('http://localhost/event/123?tab=details');
164
+ expect(r.params).toEqual({ eventId: '123' });
165
+ expect(r.searchParams).toEqual({ tab: 'details' });
172
166
  });
173
167
  });
174
168
  describe('parseUrl — fallback and notFound', () => {
@@ -249,17 +243,38 @@ describe('navigate()', () => {
249
243
  }, 'event');
250
244
  await expect(r.navigate('event')).rejects.toThrow('Missing parameter eventId');
251
245
  });
252
- it('appends search params to the path', async () => {
246
+ it('appends search params when provided', async () => {
253
247
  const r = makeRouter({
254
- 'password-reset': {
255
- rootPath: 'password-reset',
256
- segments: [],
257
- searchParams: ['token'],
258
- },
248
+ 'password-reset': { rootPath: 'password-reset', segments: [] },
259
249
  }, 'password-reset');
260
- await r.navigate('password-reset', { token: 'abc' });
250
+ await r.navigate('password-reset', undefined, { token: 'abc' });
261
251
  expect(history.pushState).toHaveBeenCalledWith({}, '', '/password-reset?token=abc');
262
252
  });
253
+ it('appends multiple search params', async () => {
254
+ const r = makeRouter({
255
+ items: { rootPath: 'items', segments: [] },
256
+ }, 'items');
257
+ await r.navigate('items', undefined, { page: '2', sort: 'asc' });
258
+ const call = history.pushState.mock.calls[0][2];
259
+ const url = new URL(call, 'http://localhost');
260
+ expect(url.pathname).toBe('/items');
261
+ expect(url.searchParams.get('page')).toBe('2');
262
+ expect(url.searchParams.get('sort')).toBe('asc');
263
+ });
264
+ it('navigates with both path params and search params', async () => {
265
+ const r = makeRouter({
266
+ event: { rootPath: 'event', segments: [{ name: 'eventId' }] },
267
+ }, 'event');
268
+ await r.navigate('event', { eventId: '42' }, { tab: 'details' });
269
+ expect(history.pushState).toHaveBeenCalledWith({}, '', '/event/42?tab=details');
270
+ });
271
+ it('does not append search params when not provided', async () => {
272
+ const r = makeRouter({
273
+ event: { rootPath: 'event', segments: [{ name: 'eventId' }] },
274
+ }, 'event');
275
+ await r.navigate('event', { eventId: '42' });
276
+ expect(history.pushState).toHaveBeenCalledWith({}, '', '/event/42');
277
+ });
263
278
  it('redirects when a guard fn returns true', async () => {
264
279
  const r = makeRouter({
265
280
  login: { rootPath: 'login', segments: [] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-navigator-lite",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "A lightweight router for Svelte 5",
5
5
  "readme": "README.md",
6
6
  "repository": {