@marianmeres/simple-router 3.0.1 → 3.3.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/API.md ADDED
@@ -0,0 +1,597 @@
1
+ # API Reference
2
+
3
+ Full API documentation for `@marianmeres/simple-router`.
4
+
5
+ ## Table of Contents
6
+
7
+ - [SimpleRouter](#simplerouter)
8
+ - [Constructor](#constructor)
9
+ - [Static Properties](#static-properties)
10
+ - [Instance Properties](#instance-properties)
11
+ - [Methods](#methods)
12
+ - [SimpleRoute](#simpleroute)
13
+ - [Constructor](#simpleroute-constructor)
14
+ - [Static Properties](#simpleroute-static-properties)
15
+ - [Static Methods](#static-methods)
16
+ - [Instance Methods](#instance-methods)
17
+ - [Types](#types)
18
+ - [Pattern Syntax](#pattern-syntax)
19
+
20
+ ---
21
+
22
+ ## SimpleRouter
23
+
24
+ The main router class for registering routes, executing pattern matching, and subscribing to state changes.
25
+
26
+ `SimpleRouter` is generic over `T`, the return type of route callbacks. This allows for type-safe `exec()` return values.
27
+
28
+ ### Constructor
29
+
30
+ ```ts
31
+ new SimpleRouter<T = unknown>(config?: RouterConfig<T> | RouterOptions<T> | null)
32
+ ```
33
+
34
+ Creates a new router instance.
35
+
36
+ **Type Parameters:**
37
+
38
+ | Name | Default | Description |
39
+ |------|---------|-------------|
40
+ | `T` | `unknown` | The return type of route callbacks |
41
+
42
+ **Parameters:**
43
+
44
+ | Name | Type | Description |
45
+ |------|------|-------------|
46
+ | `config` | `RouterConfig<T> \| RouterOptions<T> \| null` | Optional route configuration or options object |
47
+
48
+ **Example:**
49
+
50
+ ```ts
51
+ // Empty router (T defaults to unknown)
52
+ const router = new SimpleRouter();
53
+
54
+ // Typed router - all callbacks must return Component
55
+ const router = new SimpleRouter<Component>({
56
+ "/": () => HomePage,
57
+ "/about": () => AboutPage,
58
+ "*": () => NotFoundPage
59
+ });
60
+
61
+ // Simple config without explicit type (T inferred from callbacks)
62
+ const router = new SimpleRouter({
63
+ "/": () => HomePage,
64
+ "/about": () => AboutPage,
65
+ "*": () => NotFoundPage
66
+ });
67
+
68
+ // With options object (for logger support)
69
+ const router = new SimpleRouter<Component>({
70
+ routes: {
71
+ "/": () => HomePage,
72
+ "/about": () => AboutPage
73
+ },
74
+ logger: myLogger // optional, compatible with @marianmeres/clog
75
+ });
76
+ ```
77
+
78
+ ### Static Properties
79
+
80
+ #### `debug`
81
+
82
+ ```ts
83
+ static debug: boolean = false
84
+ ```
85
+
86
+ Enable/disable console debug logging for route matching. When enabled, logs detailed matching information using the logger instance (if provided) or falls back to `console.log`.
87
+
88
+ **Example:**
89
+
90
+ ```ts
91
+ SimpleRouter.debug = true;
92
+ router.exec("/test"); // Logs: [SimpleRouter] '/test' -> matches '/' with {}
93
+ ```
94
+
95
+ ### Instance Properties
96
+
97
+ #### `current`
98
+
99
+ ```ts
100
+ get current(): RouterCurrent
101
+ ```
102
+
103
+ Returns the current router state containing the last matched route, extracted parameters, and label.
104
+
105
+ **Returns:** `RouterCurrent` - Current state object
106
+
107
+ **Example:**
108
+
109
+ ```ts
110
+ router.exec("/user/123");
111
+ console.log(router.current);
112
+ // { route: "/user/[id]", params: { id: "123" }, label: null }
113
+ ```
114
+
115
+ ### Methods
116
+
117
+ #### `on()`
118
+
119
+ ```ts
120
+ on(routes: string | string[], cb: RouteCallback<T>, options?: RouterOnOptions): void
121
+ ```
122
+
123
+ Registers one or more route patterns with a callback function.
124
+
125
+ **Parameters:**
126
+
127
+ | Name | Type | Description |
128
+ |------|------|-------------|
129
+ | `routes` | `string \| string[]` | Single route pattern or array of patterns |
130
+ | `cb` | `RouteCallback<T>` | Callback function executed when route matches (must return `T`) |
131
+ | `options` | `RouterOnOptions` | Optional configuration object |
132
+
133
+ **Options:**
134
+
135
+ | Name | Type | Default | Description |
136
+ |------|------|---------|-------------|
137
+ | `label` | `string \| null` | `null` | Label for debugging/identification |
138
+ | `allowQueryParams` | `boolean` | `true` | Whether to parse query parameters |
139
+ | `strict` | `boolean` | _inherits from router_ | Strict segment matching (see `RouterOptions.strict`) |
140
+
141
+ **Example:**
142
+
143
+ ```ts
144
+ // Single route
145
+ router.on("/users", () => UsersPage);
146
+
147
+ // Multiple routes to same handler
148
+ router.on(["/", "/home", "/index.html"], () => HomePage);
149
+
150
+ // With dynamic parameters
151
+ router.on("/user/[id]", (params) => UserPage(params?.id));
152
+
153
+ // With regex constraint
154
+ router.on("/post/[id([0-9]+)]", (params) => PostPage(params?.id));
155
+
156
+ // With label for debugging
157
+ router.on("/admin", () => AdminPage, { label: "admin-dashboard" });
158
+
159
+ // Disable query parameter parsing
160
+ router.on("/raw/[path]", (params) => RawPage(params), { allowQueryParams: false });
161
+
162
+ // Catch-all route (must be registered last)
163
+ router.on("*", () => NotFoundPage);
164
+ ```
165
+
166
+ **Important:** Routes are matched in registration order. First match wins!
167
+
168
+ **Shadowing warning:** When `SimpleRouter.debug` is enabled, registering a route that is strictly more specific than an already-registered route emits a `warn`-level log message (via the provided `Logger` if any, otherwise `console.warn`). The check is skipped when `debug` is off, so there is no runtime cost in production.
169
+
170
+ ---
171
+
172
+ #### `exec()`
173
+
174
+ ```ts
175
+ exec(url: string, fallbackFn?: RouteCallback<T>): T | false
176
+ ```
177
+
178
+ Executes pattern matching against the provided string.
179
+
180
+ **Parameters:**
181
+
182
+ | Name | Type | Description |
183
+ |------|------|-------------|
184
+ | `url` | `string` | String to match against registered patterns |
185
+ | `fallbackFn` | `RouteCallback<T>` | Optional fallback function if no route matches |
186
+
187
+ **Returns:** `T | false` - The value returned by the matched callback (type `T`), or `false` if no match and no catch-all/fallback.
188
+
189
+ **Match Priority:**
190
+ 1. First registered matching route
191
+ 2. Fallback function (if provided)
192
+ 3. Catch-all `"*"` route (if registered)
193
+ 4. Returns `false`
194
+
195
+ **Example:**
196
+
197
+ ```ts
198
+ // Basic usage
199
+ const result = router.exec("/users");
200
+
201
+ // With query parameters
202
+ router.exec("/search?q=hello&page=1");
203
+
204
+ // With fallback function
205
+ router.exec("/unknown", (params, route) => {
206
+ console.log("No match found");
207
+ return NotFoundPage;
208
+ });
209
+
210
+ // Return values from callbacks
211
+ const component = router.exec("/home");
212
+ render(component);
213
+ ```
214
+
215
+ ---
216
+
217
+ #### `subscribe()`
218
+
219
+ ```ts
220
+ subscribe(subscription: RouterSubscriber): RouterUnsubscribe
221
+ ```
222
+
223
+ Subscribes to router state changes. Follows the [Svelte store contract](https://svelte.dev/docs#Store_contract).
224
+
225
+ **Parameters:**
226
+
227
+ | Name | Type | Description |
228
+ |------|------|-------------|
229
+ | `subscription` | `RouterSubscriber` | Callback function receiving state changes |
230
+
231
+ **Returns:** `RouterUnsubscribe` - Unsubscribe function
232
+
233
+ **Behavior:**
234
+ - Subscriber is called immediately with current state
235
+ - Subsequently called on every `exec()` call
236
+
237
+ **Example:**
238
+
239
+ ```ts
240
+ const unsubscribe = router.subscribe((state) => {
241
+ console.log("Route:", state.route);
242
+ console.log("Params:", state.params);
243
+ console.log("Label:", state.label);
244
+ });
245
+
246
+ // In Svelte
247
+ $: currentRoute = $router;
248
+
249
+ // Cleanup
250
+ unsubscribe();
251
+ ```
252
+
253
+ ---
254
+
255
+ #### `reset()`
256
+
257
+ ```ts
258
+ reset(): this
259
+ ```
260
+
261
+ Clears all registered routes except the catch-all (`*`).
262
+
263
+ **Returns:** The router instance for method chaining
264
+
265
+ **Example:**
266
+
267
+ ```ts
268
+ router.reset().on("/new-route", () => NewPage);
269
+ ```
270
+
271
+ ---
272
+
273
+ #### `info()`
274
+
275
+ ```ts
276
+ info(): Record<string, string>
277
+ ```
278
+
279
+ Returns a map of registered route patterns to their labels.
280
+
281
+ **Returns:** Object mapping route patterns to labels (empty string if no label)
282
+
283
+ **Example:**
284
+
285
+ ```ts
286
+ router.on("/users", () => {}, { label: "users-list" });
287
+ router.on("/posts", () => {}, { label: "posts-list" });
288
+
289
+ console.log(router.info());
290
+ // { "/users": "users-list", "/posts": "posts-list" }
291
+ ```
292
+
293
+ ---
294
+
295
+ ## SimpleRoute
296
+
297
+ Low-level route pattern parser. Use this directly only when you need pattern matching without the full router functionality.
298
+
299
+ <a id="simpleroute-constructor"></a>
300
+ ### Constructor
301
+
302
+ ```ts
303
+ new SimpleRoute(pattern: string, options?: SimpleRouteOptions)
304
+ ```
305
+
306
+ Creates a new route pattern parser.
307
+
308
+ **Parameters:**
309
+
310
+ | Name | Type | Description |
311
+ |------|------|-------------|
312
+ | `pattern` | `string` | Route pattern to match against |
313
+ | `options` | `SimpleRouteOptions` | Optional configuration (currently `{ strict?: boolean }`) |
314
+
315
+ **Throws:** `Error` if pattern is invalid — multiple spread segments, invalid regex, invalid parameter name in a named constraint, or an optional segment followed by a required one.
316
+
317
+ **Example:**
318
+
319
+ ```ts
320
+ const route = new SimpleRoute("/user/[id([0-9]+)]");
321
+ const route2 = new SimpleRoute("/files/[...path]");
322
+
323
+ // Strict mode — '/a//b' will NOT match '/a/b'
324
+ const strict = new SimpleRoute("/a/b", { strict: true });
325
+ ```
326
+
327
+ <a id="simpleroute-static-properties"></a>
328
+ ### Static Properties
329
+
330
+ #### `SPLITTER`
331
+
332
+ ```ts
333
+ static SPLITTER: string = "/"
334
+ ```
335
+
336
+ Separator used to split paths into segments.
337
+
338
+ #### `WILDCARD`
339
+
340
+ ```ts
341
+ static WILDCARD: string = "*"
342
+ ```
343
+
344
+ Wildcard symbol for catch-all routes. Must be used as the last segment.
345
+
346
+ ### Static Methods
347
+
348
+ #### `parseQueryString()`
349
+
350
+ ```ts
351
+ static parseQueryString(str: string): RouteParams
352
+ ```
353
+
354
+ Parses a query string into an object of key-value pairs.
355
+
356
+ **Parameters:**
357
+
358
+ | Name | Type | Description |
359
+ |------|------|-------------|
360
+ | `str` | `string` | Query string (without leading "?") |
361
+
362
+ **Returns:** `RouteParams` - Object with parsed query parameters
363
+
364
+ **Example:**
365
+
366
+ ```ts
367
+ SimpleRoute.parseQueryString("foo=bar&baz=123");
368
+ // { foo: "bar", baz: "123" }
369
+
370
+ SimpleRoute.parseQueryString("name=John%20Doe&city=New%20York");
371
+ // { name: "John Doe", city: "New York" }
372
+
373
+ SimpleRoute.parseQueryString("empty=&flag");
374
+ // { empty: "", flag: "" }
375
+ ```
376
+
377
+ ### Instance Methods
378
+
379
+ #### `parse()`
380
+
381
+ ```ts
382
+ parse(url: string, allowQueryParams?: boolean): RouteParams | null
383
+ ```
384
+
385
+ Parses a string against this route pattern.
386
+
387
+ **Parameters:**
388
+
389
+ | Name | Type | Default | Description |
390
+ |------|------|---------|-------------|
391
+ | `url` | `string` | - | String to test against the pattern |
392
+ | `allowQueryParams` | `boolean` | `true` | Whether to parse query string parameters |
393
+
394
+ **Returns:** `RouteParams` object with extracted parameters, or `null` if no match
395
+
396
+ **Example:**
397
+
398
+ ```ts
399
+ const route = new SimpleRoute("/user/[id]");
400
+
401
+ route.parse("/user/123");
402
+ // { id: "123" }
403
+
404
+ route.parse("/user/123?tab=profile");
405
+ // { id: "123", tab: "profile" }
406
+
407
+ route.parse("/user/123?tab=profile", false);
408
+ // { id: "123?tab=profile" }
409
+
410
+ route.parse("/post/456");
411
+ // null
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Types
417
+
418
+ ### RouteParams
419
+
420
+ ```ts
421
+ type RouteParams = Record<string, any>;
422
+ ```
423
+
424
+ Object containing extracted route parameters. Keys are parameter names, values are the extracted string values.
425
+
426
+ ### RouteCallback\<T\>
427
+
428
+ ```ts
429
+ type RouteCallback<T = unknown> = (params: RouteParams | null, route: string) => T;
430
+ ```
431
+
432
+ Callback function executed when a route matches.
433
+
434
+ **Type Parameters:**
435
+ - `T` - The return type of the callback (default: `unknown`)
436
+
437
+ **Parameters:**
438
+ - `params` - Extracted route parameters (or `null` if no params)
439
+ - `route` - The matched route pattern string
440
+
441
+ **Returns:** `T` - The value to be returned by `exec()`
442
+
443
+ ### RouterConfig\<T\>
444
+
445
+ ```ts
446
+ type RouterConfig<T = unknown> = Record<string, RouteCallback<T>>;
447
+ ```
448
+
449
+ Configuration object mapping route patterns to callbacks.
450
+
451
+ **Type Parameters:**
452
+ - `T` - The return type of all route callbacks (default: `unknown`)
453
+
454
+ ### RouterCurrent
455
+
456
+ ```ts
457
+ interface RouterCurrent {
458
+ route: string | null; // Matched pattern, "*" for catch-all, null if no match
459
+ params: RouteParams | null;
460
+ label: string | null;
461
+ }
462
+ ```
463
+
464
+ Current router state object.
465
+
466
+ ### RouterOnOptions
467
+
468
+ ```ts
469
+ interface RouterOnOptions {
470
+ label?: string | null; // Optional label for debugging
471
+ allowQueryParams?: boolean; // Whether to parse query params (default: true)
472
+ strict?: boolean; // Per-route strict mode (overrides router default)
473
+ }
474
+ ```
475
+
476
+ Options for the `on()` method.
477
+
478
+ ### SimpleRouteOptions
479
+
480
+ ```ts
481
+ interface SimpleRouteOptions {
482
+ strict?: boolean; // Disable empty-segment collapsing (default: false)
483
+ }
484
+ ```
485
+
486
+ Options for the `SimpleRoute` constructor.
487
+
488
+ ### Logger
489
+
490
+ ```ts
491
+ interface Logger {
492
+ debug: (...args: unknown[]) => unknown;
493
+ log: (...args: unknown[]) => unknown;
494
+ warn: (...args: unknown[]) => unknown;
495
+ error: (...args: unknown[]) => unknown;
496
+ }
497
+ ```
498
+
499
+ Logger interface compatible with `@marianmeres/clog`. Provides console-compatible logging methods.
500
+
501
+ ### RouterOptions\<T\>
502
+
503
+ ```ts
504
+ interface RouterOptions<T = unknown> {
505
+ routes?: RouterConfig<T> | null; // Route configuration
506
+ logger?: Logger | null; // Optional logger instance
507
+ strict?: boolean; // Strict mode; propagates to every route
508
+ }
509
+ ```
510
+
511
+ Options object for the constructor (alternative to plain `RouterConfig`).
512
+
513
+ **Type Parameters:**
514
+ - `T` - The return type of route callbacks (default: `unknown`)
515
+
516
+ **`strict` mode:** when `true`, empty segments in both patterns and inputs (e.g. `/a//b`) are preserved rather than collapsed. This means `/a//b` will NOT match the canonical pattern `/a/b` — useful for URL canonicalization and preventing accidental shadow-matching in environments where concatenation bugs can produce doubled separators. Per-route `RouterOnOptions.strict` overrides this default.
517
+
518
+ ### RouterSubscriber
519
+
520
+ ```ts
521
+ type RouterSubscriber = (current: RouterCurrent) => void;
522
+ ```
523
+
524
+ Callback function for route subscriptions.
525
+
526
+ ### RouterUnsubscribe
527
+
528
+ ```ts
529
+ type RouterUnsubscribe = () => void;
530
+ ```
531
+
532
+ Unsubscribe function returned by `subscribe()`.
533
+
534
+ ### RouteConfig (Internal)
535
+
536
+ ```ts
537
+ interface RouteConfig {
538
+ segment: string;
539
+ name: string | null;
540
+ test: RegExp;
541
+ isOptional: boolean;
542
+ isSpread: boolean;
543
+ }
544
+ ```
545
+
546
+ Internal configuration for a parsed route segment. Exported for advanced use cases.
547
+
548
+ ---
549
+
550
+ ## Pattern Syntax
551
+
552
+ ### Segment Types
553
+
554
+ | Pattern | Description | Example |
555
+ |---------|-------------|---------|
556
+ | `exact` | Literal match | `foo` matches "foo" |
557
+ | `[name]` | Named parameter | `[id]` captures any segment |
558
+ | `[name(regex)]` | Regex constrained | `[id([0-9]+)]` captures digits only |
559
+ | `[name]?` | Optional segment | `[id]?` matches with or without |
560
+ | `[...name]` | Spread parameter | `[...path]` captures multiple segments |
561
+ | `*` | Wildcard catch-all | Matches any remaining path |
562
+
563
+ ### Key Behaviors
564
+
565
+ 1. **Separator Normalization:** Multiple separators are collapsed in non-strict mode (default); trimmed from ends. Enable `strict: true` to preserve them.
566
+ 2. **First Match Wins:** Routes matched in registration order. Enable `SimpleRouter.debug` for shadowing warnings at registration time.
567
+ 3. **Query String Parsing:** Enabled by default, can disable per-route.
568
+ 4. **Path Params Priority:** Path params override query params with the same name.
569
+ 5. **URL Decoding:** Parameter _values_ are URL-decoded. Parameter _names_ are taken literally from the pattern (not decoded).
570
+
571
+ ### Pattern Constraints
572
+
573
+ - Spread segments cannot be optional (`[...path]?` throws error)
574
+ - Only one spread segment allowed per route
575
+ - Wildcard `*` must be the last segment
576
+ - Regex in `[name(regex)]` must be valid JavaScript regex
577
+ - In a named constraint `[name(regex)]`, `name` must match `/^\w+$/`
578
+ - An optional segment (`[x]?`) may not be followed by a required segment — optional segments are only allowed in trailing position (or directly before a wildcard `*`)
579
+
580
+ ### Security (regex constraints)
581
+
582
+ Regex constraints are compiled as ordinary JavaScript regexes. Some patterns (notably nested quantifiers like `(a+)+`) can exhibit catastrophic backtracking on crafted inputs. Route patterns are author-controlled — **never** accept them from untrusted input. Prefer simple character classes (`[0-9]+`, `[a-z]+`) over nested quantifiers.
583
+
584
+ ### Examples
585
+
586
+ | Pattern | Input | Result |
587
+ |---------|-------|--------|
588
+ | `/foo` | `/bar` | `null` |
589
+ | `/foo` | `/foo` | `{}` |
590
+ | `/[foo]` | `/bar` | `{ foo: "bar" }` |
591
+ | `/[id([0-9]+)]` | `/123` | `{ id: "123" }` |
592
+ | `/[id([0-9]+)]` | `/abc` | `null` |
593
+ | `/foo/[bar]?` | `/foo` | `{}` |
594
+ | `/foo/[bar]?` | `/foo/baz` | `{ bar: "baz" }` |
595
+ | `/[...path]/[file]` | `/a/b/c.js` | `{ path: "a/b", file: "c.js" }` |
596
+ | `/*` | `/any/path` | `{}` |
597
+ | `/[foo]/*` | `/bar/baz` | `{ foo: "bar" }` |
package/CLAUDE.md ADDED
@@ -0,0 +1,3 @@
1
+ # Project Instructions
2
+
3
+ See [AGENTS.md](./AGENTS.md) for complete project documentation and AI agent instructions.