@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/AGENTS.md +399 -0
- package/API.md +597 -0
- package/CLAUDE.md +3 -0
- package/README.md +85 -13
- package/dist/mod.d.ts +1 -1
- package/dist/route.d.ts +18 -2
- package/dist/route.js +65 -17
- package/dist/router.d.ts +43 -26
- package/dist/router.js +56 -25
- package/package.json +19 -6
package/README.md
CHANGED
|
@@ -77,7 +77,35 @@ window.onhashchange = () => {
|
|
|
77
77
|
|
|
78
78
|
### Separators
|
|
79
79
|
|
|
80
|
-
The default separator is `/`.
|
|
80
|
+
The default separator is `/`. By default (non-strict mode), multiple consecutive separators are collapsed into a single one, and separators are trimmed from both ends before matching. So `/foo//bar` and `/foo/bar/` both resolve to `/foo/bar`.
|
|
81
|
+
|
|
82
|
+
If you need to treat consecutive separators as significant (for example, to canonicalize URLs and prevent accidental shadow-matching), enable **strict mode**:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// At router level (propagates to every route):
|
|
86
|
+
const router = new SimpleRouter({ strict: true, routes: { "/a/b": handler } });
|
|
87
|
+
router.exec("/a/b"); // matches
|
|
88
|
+
router.exec("/a//b"); // false (no match)
|
|
89
|
+
|
|
90
|
+
// Per-route override:
|
|
91
|
+
router.on("/legacy", handler, { strict: true });
|
|
92
|
+
|
|
93
|
+
// Or directly on SimpleRoute:
|
|
94
|
+
new SimpleRoute("/a/b", { strict: true });
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Optional segments
|
|
98
|
+
|
|
99
|
+
Optional segments (`[name]?`) are only allowed in **trailing position** (or directly before a trailing wildcard `*`). Optional-followed-by-required patterns are rejected at construction:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
new SimpleRoute("/foo/[bar]?/baz");
|
|
103
|
+
// Throws: optional segment '[bar]?' must not be followed by a required segment
|
|
104
|
+
|
|
105
|
+
// Instead, register two routes:
|
|
106
|
+
router.on("/foo/[bar]/baz", handler);
|
|
107
|
+
router.on("/foo/baz", handler);
|
|
108
|
+
```
|
|
81
109
|
|
|
82
110
|
## Pattern Examples
|
|
83
111
|
|
|
@@ -96,7 +124,6 @@ The default separator is `/`. Multiple separators are normalized to single, and
|
|
|
96
124
|
| `/foo/[bar]/[id([0-9]+)]` | `/foo/baz/123` | `{ bar: "baz", id: "123" }` |
|
|
97
125
|
| `/foo/[bar]?` | `/foo` | `{}` |
|
|
98
126
|
| `/foo/[bar]?` | `/foo/bar` | `{ bar: "bar" }` |
|
|
99
|
-
| `/foo/[bar]?/baz` | `/foo/bar/baz` | `{ bar: "bar" }` |
|
|
100
127
|
| `/[...path]/[file]` | `/foo/bar/baz.js` | `{ path: "foo/bar", file: "baz.js" }` |
|
|
101
128
|
| `/foo/*` | `/foo/bar/baz.js` | `{}` |
|
|
102
129
|
| `/[foo]/*` | `/foo/bar` | `{ foo: "foo" }` |
|
|
@@ -126,7 +153,7 @@ const router = new SimpleRouter({
|
|
|
126
153
|
});
|
|
127
154
|
```
|
|
128
155
|
|
|
129
|
-
- `config` - Either a `RouterConfig` object mapping route patterns to callbacks, or a `RouterOptions` object with `routes` and `
|
|
156
|
+
- `config` - Either a `RouterConfig` object mapping route patterns to callbacks, or a `RouterOptions` object with `routes`, `logger`, and `strict` properties
|
|
130
157
|
|
|
131
158
|
#### Methods
|
|
132
159
|
|
|
@@ -156,8 +183,9 @@ router.on("/raw", (params) => RawPage, { allowQueryParams: false });
|
|
|
156
183
|
**Options:**
|
|
157
184
|
- `label` - Optional label for debugging (visible via `info()`)
|
|
158
185
|
- `allowQueryParams` - Whether to parse query parameters (default: `true`)
|
|
186
|
+
- `strict` - Per-route strict matching (overrides router default)
|
|
159
187
|
|
|
160
|
-
**Important:** Routes are matched in registration order. First match wins!
|
|
188
|
+
**Important:** Routes are matched in registration order. First match wins! When `SimpleRouter.debug` is enabled, a warning is logged at registration time if a new route would be shadowed by an already-registered one.
|
|
161
189
|
|
|
162
190
|
##### `exec(url, fallbackFn?)`
|
|
163
191
|
|
|
@@ -255,19 +283,46 @@ SimpleRoute.parseQueryString("foo=bar&baz=123");
|
|
|
255
283
|
|
|
256
284
|
## TypeScript
|
|
257
285
|
|
|
258
|
-
Full TypeScript support with
|
|
286
|
+
Full TypeScript support with generic types for type-safe route callbacks and `exec()` return values.
|
|
287
|
+
|
|
288
|
+
### Generic Router
|
|
289
|
+
|
|
290
|
+
`SimpleRouter<T>` is generic over `T`, the return type of route callbacks:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
// Typed router - all callbacks must return Component, exec() returns Component | false
|
|
294
|
+
const router = new SimpleRouter<Component>({
|
|
295
|
+
"/": () => HomePage,
|
|
296
|
+
"/about": () => AboutPage,
|
|
297
|
+
"*": () => NotFoundPage,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const result = router.exec("/about"); // Component | false
|
|
301
|
+
if (result !== false) {
|
|
302
|
+
render(result); // result is Component
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Without explicit type - T is inferred from callbacks (or defaults to unknown)
|
|
306
|
+
const router2 = new SimpleRouter({
|
|
307
|
+
"/": () => "home",
|
|
308
|
+
"/about": () => "about",
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Exported Types
|
|
259
313
|
|
|
260
314
|
```ts
|
|
261
315
|
import type {
|
|
262
316
|
Logger,
|
|
263
317
|
RouteParams,
|
|
264
|
-
RouteCallback,
|
|
265
|
-
RouterConfig,
|
|
318
|
+
RouteCallback, // RouteCallback<T = unknown>
|
|
319
|
+
RouterConfig, // RouterConfig<T = unknown>
|
|
266
320
|
RouterCurrent,
|
|
267
321
|
RouterOnOptions,
|
|
268
|
-
RouterOptions,
|
|
322
|
+
RouterOptions, // RouterOptions<T = unknown>
|
|
269
323
|
RouterSubscriber,
|
|
270
324
|
RouterUnsubscribe,
|
|
325
|
+
SimpleRouteOptions, // { strict?: boolean }
|
|
271
326
|
} from "@marianmeres/simple-router";
|
|
272
327
|
```
|
|
273
328
|
|
|
@@ -276,21 +331,24 @@ import type {
|
|
|
276
331
|
### SPA with Hash Routing
|
|
277
332
|
|
|
278
333
|
```ts
|
|
279
|
-
|
|
334
|
+
// Type-safe router with Component return type
|
|
335
|
+
const router = new SimpleRouter<Component>({
|
|
280
336
|
"/": () => HomePage,
|
|
281
337
|
"/about": () => AboutPage,
|
|
282
338
|
"/user/[id]": (params) => UserPage(params?.id),
|
|
283
339
|
"*": () => NotFoundPage,
|
|
284
340
|
});
|
|
285
341
|
|
|
286
|
-
function render(component) {
|
|
287
|
-
document.getElementById("app").innerHTML = component;
|
|
342
|
+
function render(component: Component) {
|
|
343
|
+
document.getElementById("app").innerHTML = component.render();
|
|
288
344
|
}
|
|
289
345
|
|
|
290
346
|
window.addEventListener("hashchange", () => {
|
|
291
347
|
const path = location.hash.slice(1) || "/";
|
|
292
|
-
const component = router.exec(path);
|
|
293
|
-
|
|
348
|
+
const component = router.exec(path); // Component | false
|
|
349
|
+
if (component !== false) {
|
|
350
|
+
render(component);
|
|
351
|
+
}
|
|
294
352
|
});
|
|
295
353
|
|
|
296
354
|
// Trigger initial render
|
|
@@ -365,5 +423,19 @@ dotRouter.exec("app.settings.profile");
|
|
|
365
423
|
// Returns: "Settings: profile"
|
|
366
424
|
```
|
|
367
425
|
|
|
426
|
+
## Security notes
|
|
427
|
+
|
|
428
|
+
Regex constraints inside a route pattern (e.g. `[id([0-9]+)]`) are compiled with `RegExp`, so any valid JavaScript regex is accepted. A handful of patterns — typically nested quantifiers such as `(a+)+` — can exhibit catastrophic backtracking (ReDoS) on crafted inputs.
|
|
429
|
+
|
|
430
|
+
- Route patterns are author-controlled: **never** accept them from untrusted input.
|
|
431
|
+
- Prefer simple character classes (`[0-9]+`, `[a-z]+`) over nested quantifiers.
|
|
432
|
+
- If you need a complex constraint, test it against adversarial inputs first.
|
|
433
|
+
|
|
434
|
+
## Breaking changes
|
|
435
|
+
|
|
436
|
+
### 3.2.0
|
|
368
437
|
|
|
438
|
+
- **Optional-followed-by-required is now rejected at construction.** Patterns like `/foo/[bar]?/baz` previously parsed but silently treated the optional marker as required — `/foo/baz` never matched. Such patterns now throw with a clear error. Register two separate routes instead (`/foo/[bar]/baz` and `/foo/baz`).
|
|
439
|
+
- **Parameter names are no longer URI-decoded.** A pattern like `/foo/[id%20x]` used to produce the key `id x` in the params object; it now produces `id%20x`. Param values are still decoded. Parameter names in a named constraint (`[name(regex)]`) must now match `\w+`.
|
|
440
|
+
- **Regex constraints with parentheses now work correctly.** Patterns like `/[id((?:a|b)+)]` previously produced garbage regexes or threw obscure "Invalid regular expression" errors; they now parse and match as expected.
|
|
369
441
|
|
package/dist/mod.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { SimpleRoute } from "./route.js";
|
|
2
2
|
export { SimpleRouter } from "./router.js";
|
|
3
|
-
export type { RouteConfig, RouteParams } from "./route.js";
|
|
3
|
+
export type { RouteConfig, RouteParams, SimpleRouteOptions } from "./route.js";
|
|
4
4
|
export type { Logger, RouteCallback, RouterConfig, RouterCurrent, RouterOnOptions, RouterOptions, RouterSubscriber, RouterUnsubscribe, } from "./router.js";
|
package/dist/route.d.ts
CHANGED
|
@@ -13,6 +13,17 @@ export interface RouteConfig {
|
|
|
13
13
|
* Route parameters extracted from URL segments and query strings.
|
|
14
14
|
*/
|
|
15
15
|
export type RouteParams = Record<string, any>;
|
|
16
|
+
/**
|
|
17
|
+
* Options for the `SimpleRoute` constructor.
|
|
18
|
+
*/
|
|
19
|
+
export interface SimpleRouteOptions {
|
|
20
|
+
/**
|
|
21
|
+
* When true, empty segments (caused by consecutive separators) are preserved
|
|
22
|
+
* rather than silently collapsed. In strict mode, inputs like `/a//b` will
|
|
23
|
+
* NOT match a pattern `/a/b`. Default: false.
|
|
24
|
+
*/
|
|
25
|
+
strict?: boolean;
|
|
26
|
+
}
|
|
16
27
|
export declare class SimpleRoute {
|
|
17
28
|
#private;
|
|
18
29
|
readonly route: string;
|
|
@@ -30,7 +41,9 @@ export declare class SimpleRoute {
|
|
|
30
41
|
* Creates a new SimpleRoute instance.
|
|
31
42
|
*
|
|
32
43
|
* @param route - Pattern string to match against (URL, file path, command, or any string identifier)
|
|
33
|
-
* @
|
|
44
|
+
* @param options - Optional configuration (e.g. `{ strict: true }` to disable empty-segment collapsing)
|
|
45
|
+
* @throws {Error} If route pattern is invalid (multiple spread segments, optional
|
|
46
|
+
* segment followed by a required one, invalid regex constraint, etc.)
|
|
34
47
|
*
|
|
35
48
|
* @example
|
|
36
49
|
* ```ts
|
|
@@ -44,9 +57,12 @@ export declare class SimpleRoute {
|
|
|
44
57
|
*
|
|
45
58
|
* // Command patterns
|
|
46
59
|
* new SimpleRoute("user:delete:[id]");
|
|
60
|
+
*
|
|
61
|
+
* // Strict mode: '/a//b' will NOT match '/a/b'
|
|
62
|
+
* new SimpleRoute("/a/b", { strict: true });
|
|
47
63
|
* ```
|
|
48
64
|
*/
|
|
49
|
-
constructor(route: string);
|
|
65
|
+
constructor(route: string, options?: SimpleRouteOptions);
|
|
50
66
|
/**
|
|
51
67
|
* Parses a query string into an object of key-value pairs.
|
|
52
68
|
* Handles URL decoding of both keys and values.
|
package/dist/route.js
CHANGED
|
@@ -11,11 +11,14 @@ export class SimpleRoute {
|
|
|
11
11
|
*/
|
|
12
12
|
static WILDCARD = "*";
|
|
13
13
|
#parsed;
|
|
14
|
+
#strict;
|
|
14
15
|
/**
|
|
15
16
|
* Creates a new SimpleRoute instance.
|
|
16
17
|
*
|
|
17
18
|
* @param route - Pattern string to match against (URL, file path, command, or any string identifier)
|
|
18
|
-
* @
|
|
19
|
+
* @param options - Optional configuration (e.g. `{ strict: true }` to disable empty-segment collapsing)
|
|
20
|
+
* @throws {Error} If route pattern is invalid (multiple spread segments, optional
|
|
21
|
+
* segment followed by a required one, invalid regex constraint, etc.)
|
|
19
22
|
*
|
|
20
23
|
* @example
|
|
21
24
|
* ```ts
|
|
@@ -29,31 +32,54 @@ export class SimpleRoute {
|
|
|
29
32
|
*
|
|
30
33
|
* // Command patterns
|
|
31
34
|
* new SimpleRoute("user:delete:[id]");
|
|
35
|
+
*
|
|
36
|
+
* // Strict mode: '/a//b' will NOT match '/a/b'
|
|
37
|
+
* new SimpleRoute("/a/b", { strict: true });
|
|
32
38
|
* ```
|
|
33
39
|
*/
|
|
34
|
-
constructor(route) {
|
|
40
|
+
constructor(route, options = {}) {
|
|
35
41
|
this.route = route;
|
|
36
|
-
this.#
|
|
42
|
+
this.#strict = options.strict === true;
|
|
43
|
+
this.#parsed = SimpleRoute.#parse(route, this.#strict);
|
|
37
44
|
}
|
|
38
45
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
|
39
46
|
static #escapeRegExp(str) {
|
|
40
47
|
return str.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&");
|
|
41
48
|
}
|
|
42
|
-
static #sanitizeAndSplit(str) {
|
|
49
|
+
static #sanitizeAndSplit(str, strict) {
|
|
43
50
|
const s = SimpleRoute.#escapeRegExp(SimpleRoute.SPLITTER);
|
|
44
|
-
|
|
51
|
+
let out = `${str}`
|
|
45
52
|
.trim()
|
|
46
53
|
// Trim splitters from left and right
|
|
47
54
|
.replace(new RegExp(`^(${s})+`), "")
|
|
48
55
|
.replace(new RegExp(`(${s})+$`), "")
|
|
49
|
-
.split(SimpleRoute.SPLITTER)
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
.split(SimpleRoute.SPLITTER);
|
|
57
|
+
// In non-strict mode, empty (internal) segments are silently collapsed.
|
|
58
|
+
// In strict mode, they are preserved and treated as literal empty segments
|
|
59
|
+
// (which generally will not match any parameterized pattern position).
|
|
60
|
+
if (!strict)
|
|
61
|
+
out = out.filter(Boolean);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Splits a bracketed definition like "name(regex)" into its two parts.
|
|
66
|
+
* Uses balanced-paren tracking so nested groups in the regex are preserved.
|
|
67
|
+
* Returns null if the input is not in the name(...) shape.
|
|
68
|
+
*/
|
|
69
|
+
static #splitNamedConstraint(inner) {
|
|
70
|
+
const open = inner.indexOf("(");
|
|
71
|
+
if (open <= 0)
|
|
72
|
+
return null;
|
|
73
|
+
if (!inner.endsWith(")"))
|
|
74
|
+
return null;
|
|
75
|
+
const name = inner.slice(0, open);
|
|
76
|
+
const regex = inner.slice(open + 1, -1);
|
|
77
|
+
return { name, regex };
|
|
52
78
|
}
|
|
53
|
-
static #parse(route) {
|
|
79
|
+
static #parse(route, strict) {
|
|
54
80
|
let wasSpread = false;
|
|
55
81
|
let wasWildcard = false;
|
|
56
|
-
|
|
82
|
+
const parsed = SimpleRoute.#sanitizeAndSplit(route, strict).reduce((memo, segment, idx, all) => {
|
|
57
83
|
let name = null;
|
|
58
84
|
// If optional, remove trailing '?' marker
|
|
59
85
|
let isOptional = segment.endsWith("?");
|
|
@@ -84,12 +110,16 @@ export class SimpleRoute {
|
|
|
84
110
|
if (m) {
|
|
85
111
|
name = m[1];
|
|
86
112
|
test = /.+/;
|
|
87
|
-
// id([0-9]+)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
// Named constraint: [id([0-9]+)], [s((?:a|b)+)], ...
|
|
114
|
+
// Split on first '(' (balanced-paren-safe).
|
|
115
|
+
const split = SimpleRoute.#splitNamedConstraint(m[1]);
|
|
116
|
+
if (split) {
|
|
117
|
+
if (!/^\w+$/.test(split.name)) {
|
|
118
|
+
throw new Error(`Invalid parameter name '${split.name}' in route pattern '${segment}' (must match /^\\w+$/)`);
|
|
119
|
+
}
|
|
120
|
+
name = split.name;
|
|
91
121
|
try {
|
|
92
|
-
test = new RegExp("^" +
|
|
122
|
+
test = new RegExp("^" + split.regex + "$");
|
|
93
123
|
}
|
|
94
124
|
catch (e) {
|
|
95
125
|
throw new Error(`Invalid regex in route pattern '${segment}': ${e.message}`);
|
|
@@ -104,6 +134,24 @@ export class SimpleRoute {
|
|
|
104
134
|
memo.push({ segment, name, test, isOptional, isSpread });
|
|
105
135
|
return memo;
|
|
106
136
|
}, []);
|
|
137
|
+
// An optional segment may only appear in trailing position (or directly
|
|
138
|
+
// before a wildcard, which itself is optional). Otherwise the 'optional'
|
|
139
|
+
// marker is misleading — it can never actually be skipped.
|
|
140
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
141
|
+
const p = parsed[i];
|
|
142
|
+
if (!p.isOptional)
|
|
143
|
+
continue;
|
|
144
|
+
// segment is optional — all following segments must also be optional
|
|
145
|
+
for (let j = i + 1; j < parsed.length; j++) {
|
|
146
|
+
if (!parsed[j].isOptional) {
|
|
147
|
+
throw new Error(`Invalid route '${route}': optional segment '${p.segment}?' must not be followed by a required segment ('${parsed[j].segment}'). ` +
|
|
148
|
+
`Optional segments are only allowed in trailing position. ` +
|
|
149
|
+
`Register separate routes instead.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
return parsed;
|
|
107
155
|
}
|
|
108
156
|
/**
|
|
109
157
|
* Parses a query string into an object of key-value pairs.
|
|
@@ -186,7 +234,7 @@ export class SimpleRoute {
|
|
|
186
234
|
url = _backup.slice(0, qPos);
|
|
187
235
|
matched = SimpleRoute.parseQueryString(_backup.slice(qPos + 1));
|
|
188
236
|
}
|
|
189
|
-
let segments = SimpleRoute.#sanitizeAndSplit(url);
|
|
237
|
+
let segments = SimpleRoute.#sanitizeAndSplit(url, this.#strict);
|
|
190
238
|
// If there are spread definitions, we need to adjust input by grouping
|
|
191
239
|
// (joining) segments that were initially split
|
|
192
240
|
const hasSpread = this.#parsed.some((v) => v.isSpread);
|
|
@@ -238,7 +286,7 @@ export class SimpleRoute {
|
|
|
238
286
|
}
|
|
239
287
|
if (p.name) {
|
|
240
288
|
try {
|
|
241
|
-
matched[
|
|
289
|
+
matched[p.name] = decodeURIComponent(s);
|
|
242
290
|
}
|
|
243
291
|
catch {
|
|
244
292
|
// Fallback for malformed URI components
|
package/dist/router.d.ts
CHANGED
|
@@ -12,19 +12,20 @@ export type RouteParams = Record<string, any>;
|
|
|
12
12
|
/**
|
|
13
13
|
* Callback function executed when a route matches.
|
|
14
14
|
*
|
|
15
|
+
* @typeParam T - The return type of the callback
|
|
15
16
|
* @param params - Extracted route parameters (null if no match)
|
|
16
17
|
* @param route - The matched route pattern string
|
|
17
18
|
* @returns Any value - useful for returning components, data, or control flow values
|
|
18
19
|
*
|
|
19
20
|
* @example
|
|
20
21
|
* ```ts
|
|
21
|
-
* const callback: RouteCallback = (params, route) => {
|
|
22
|
+
* const callback: RouteCallback<MyComponent> = (params, route) => {
|
|
22
23
|
* console.log(`Matched ${route} with params:`, params);
|
|
23
24
|
* return MyComponent;
|
|
24
25
|
* };
|
|
25
26
|
* ```
|
|
26
27
|
*/
|
|
27
|
-
export type RouteCallback = (params: RouteParams | null, route: string) =>
|
|
28
|
+
export type RouteCallback<T = unknown> = (params: RouteParams | null, route: string) => T;
|
|
28
29
|
/**
|
|
29
30
|
* Represents the current router state including the matched route,
|
|
30
31
|
* extracted parameters, and optional label.
|
|
@@ -45,21 +46,28 @@ export interface RouterOnOptions {
|
|
|
45
46
|
label?: string | null;
|
|
46
47
|
/** Whether to parse query parameters (default: true) */
|
|
47
48
|
allowQueryParams?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Strict matching mode for this route (overrides router-level default).
|
|
51
|
+
* See `RouterOptions.strict`.
|
|
52
|
+
*/
|
|
53
|
+
strict?: boolean;
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
56
|
* Configuration object for initializing the router with routes.
|
|
51
57
|
* Maps route patterns to their callback functions.
|
|
52
58
|
*
|
|
59
|
+
* @typeParam T - The return type of route callbacks
|
|
60
|
+
*
|
|
53
61
|
* @example
|
|
54
62
|
* ```ts
|
|
55
|
-
* const config: RouterConfig = {
|
|
63
|
+
* const config: RouterConfig<Component> = {
|
|
56
64
|
* "/": () => HomePage,
|
|
57
65
|
* "/user/[id]": (params) => UserPage(params?.id),
|
|
58
66
|
* "*": () => NotFoundPage
|
|
59
67
|
* };
|
|
60
68
|
* ```
|
|
61
69
|
*/
|
|
62
|
-
export type RouterConfig = Record<string, RouteCallback
|
|
70
|
+
export type RouterConfig<T = unknown> = Record<string, RouteCallback<T>>;
|
|
63
71
|
/**
|
|
64
72
|
* Logger interface compatible with `@marianmeres/clog`.
|
|
65
73
|
* Provides console-compatible logging methods.
|
|
@@ -76,12 +84,21 @@ export interface Logger {
|
|
|
76
84
|
}
|
|
77
85
|
/**
|
|
78
86
|
* Options for the SimpleRouter factory/constructor.
|
|
87
|
+
*
|
|
88
|
+
* @typeParam T - The return type of route callbacks
|
|
79
89
|
*/
|
|
80
|
-
export interface RouterOptions {
|
|
90
|
+
export interface RouterOptions<T = unknown> {
|
|
81
91
|
/** Route configuration mapping patterns to callbacks */
|
|
82
|
-
routes?: RouterConfig | null;
|
|
92
|
+
routes?: RouterConfig<T> | null;
|
|
83
93
|
/** Optional logger instance for debug output (compatible with @marianmeres/clog) */
|
|
84
94
|
logger?: Logger | null;
|
|
95
|
+
/**
|
|
96
|
+
* Strict matching mode. When true, empty segments in inputs (caused by
|
|
97
|
+
* consecutive separators like `/a//b`) are NOT silently collapsed — such
|
|
98
|
+
* inputs will not match the canonical pattern `/a/b`. Propagates to every
|
|
99
|
+
* route registered via this router. Default: false.
|
|
100
|
+
*/
|
|
101
|
+
strict?: boolean;
|
|
85
102
|
}
|
|
86
103
|
/**
|
|
87
104
|
* Unsubscribe function returned by the `subscribe()` method.
|
|
@@ -123,8 +140,10 @@ export type RouterSubscriber = (current: RouterCurrent) => void;
|
|
|
123
140
|
*
|
|
124
141
|
* cmdRouter.exec("user:delete:123");
|
|
125
142
|
* ```
|
|
143
|
+
*
|
|
144
|
+
* @typeParam T - The return type of route callbacks (default: unknown)
|
|
126
145
|
*/
|
|
127
|
-
export declare class SimpleRouter {
|
|
146
|
+
export declare class SimpleRouter<T = unknown> {
|
|
128
147
|
#private;
|
|
129
148
|
/**
|
|
130
149
|
* Enable/disable console debug logging for route matching.
|
|
@@ -140,14 +159,14 @@ export declare class SimpleRouter {
|
|
|
140
159
|
*
|
|
141
160
|
* @example
|
|
142
161
|
* ```ts
|
|
143
|
-
* //
|
|
144
|
-
* const router = new SimpleRouter({
|
|
162
|
+
* // Typed router - exec() returns Component | false
|
|
163
|
+
* const router = new SimpleRouter<Component>({
|
|
145
164
|
* "/": () => HomePage,
|
|
146
165
|
* "/about": () => AboutPage
|
|
147
166
|
* });
|
|
148
167
|
*
|
|
149
168
|
* // With options object
|
|
150
|
-
* const router = new SimpleRouter({
|
|
169
|
+
* const router = new SimpleRouter<Component>({
|
|
151
170
|
* routes: {
|
|
152
171
|
* "/": () => HomePage,
|
|
153
172
|
* "/about": () => AboutPage
|
|
@@ -156,7 +175,7 @@ export declare class SimpleRouter {
|
|
|
156
175
|
* });
|
|
157
176
|
* ```
|
|
158
177
|
*/
|
|
159
|
-
constructor(config?: RouterConfig | RouterOptions | null);
|
|
178
|
+
constructor(config?: RouterConfig<T> | RouterOptions<T> | null);
|
|
160
179
|
/**
|
|
161
180
|
* Returns a map of registered route patterns to their labels.
|
|
162
181
|
* Useful for debugging and introspection.
|
|
@@ -224,38 +243,36 @@ export declare class SimpleRouter {
|
|
|
224
243
|
* router.on("*", () => NotFoundPage);
|
|
225
244
|
* ```
|
|
226
245
|
*/
|
|
227
|
-
on(routes: string | string[], cb: RouteCallback
|
|
246
|
+
on(routes: string | string[], cb: RouteCallback<T>, options?: RouterOnOptions): void;
|
|
228
247
|
/**
|
|
229
248
|
* Executes pattern matching against the provided string.
|
|
230
|
-
* Returns the value returned by the matched route's callback.
|
|
249
|
+
* Returns the value returned by the matched route's callback (type `T`).
|
|
231
250
|
* Routes are tested in registration order - first match wins.
|
|
232
251
|
*
|
|
233
252
|
* @param url - String to match against registered patterns (can be a URL, file path, command, etc.)
|
|
234
253
|
* @param fallbackFn - Optional fallback function if no route matches
|
|
235
|
-
* @returns The value returned by the matched callback, or false if no match
|
|
254
|
+
* @returns `T | false` - The value returned by the matched callback, or `false` if no match
|
|
236
255
|
*
|
|
237
256
|
* @example
|
|
238
257
|
* ```ts
|
|
239
|
-
* //
|
|
240
|
-
* router
|
|
258
|
+
* // Type-safe routing
|
|
259
|
+
* const router = new SimpleRouter<Component>({ "/home": () => HomePage });
|
|
260
|
+
* const result = router.exec("/home"); // Component | false
|
|
241
261
|
*
|
|
242
262
|
* // With query parameters
|
|
243
263
|
* router.exec("/users?sort=name");
|
|
244
264
|
*
|
|
245
|
-
* // File path matching
|
|
246
|
-
* router.exec("src/components/Button.ts");
|
|
247
|
-
*
|
|
248
|
-
* // Command matching
|
|
249
|
-
* router.exec("user:delete:123");
|
|
250
|
-
*
|
|
251
265
|
* // With fallback
|
|
252
|
-
* router.exec("/unknown", () =>
|
|
266
|
+
* router.exec("/unknown", () => NotFoundPage);
|
|
253
267
|
*
|
|
254
|
-
* //
|
|
255
|
-
* const component = router.exec("/
|
|
268
|
+
* // Check for no match
|
|
269
|
+
* const component = router.exec("/path");
|
|
270
|
+
* if (component !== false) {
|
|
271
|
+
* render(component);
|
|
272
|
+
* }
|
|
256
273
|
* ```
|
|
257
274
|
*/
|
|
258
|
-
exec(url: string, fallbackFn?: RouteCallback):
|
|
275
|
+
exec(url: string, fallbackFn?: RouteCallback<T>): T | false;
|
|
259
276
|
/**
|
|
260
277
|
* Subscribes to router state changes.
|
|
261
278
|
* Follows the Svelte store contract - subscriber is called immediately with current state.
|