@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/dist/router.js
CHANGED
|
@@ -30,6 +30,8 @@ const PUBSUB_TOPIC = "current";
|
|
|
30
30
|
*
|
|
31
31
|
* cmdRouter.exec("user:delete:123");
|
|
32
32
|
* ```
|
|
33
|
+
*
|
|
34
|
+
* @typeParam T - The return type of route callbacks (default: unknown)
|
|
33
35
|
*/
|
|
34
36
|
export class SimpleRouter {
|
|
35
37
|
/**
|
|
@@ -40,6 +42,7 @@ export class SimpleRouter {
|
|
|
40
42
|
static debug = false;
|
|
41
43
|
#routes = [];
|
|
42
44
|
#catchAll = null;
|
|
45
|
+
#strict = false;
|
|
43
46
|
#current = {
|
|
44
47
|
route: null,
|
|
45
48
|
params: null,
|
|
@@ -55,14 +58,14 @@ export class SimpleRouter {
|
|
|
55
58
|
*
|
|
56
59
|
* @example
|
|
57
60
|
* ```ts
|
|
58
|
-
* //
|
|
59
|
-
* const router = new SimpleRouter({
|
|
61
|
+
* // Typed router - exec() returns Component | false
|
|
62
|
+
* const router = new SimpleRouter<Component>({
|
|
60
63
|
* "/": () => HomePage,
|
|
61
64
|
* "/about": () => AboutPage
|
|
62
65
|
* });
|
|
63
66
|
*
|
|
64
67
|
* // With options object
|
|
65
|
-
* const router = new SimpleRouter({
|
|
68
|
+
* const router = new SimpleRouter<Component>({
|
|
66
69
|
* routes: {
|
|
67
70
|
* "/": () => HomePage,
|
|
68
71
|
* "/about": () => AboutPage
|
|
@@ -74,11 +77,12 @@ export class SimpleRouter {
|
|
|
74
77
|
constructor(config = null) {
|
|
75
78
|
let routes = null;
|
|
76
79
|
if (config) {
|
|
77
|
-
// Check if config is RouterOptions (has 'routes' or '
|
|
78
|
-
if ("routes" in config || "logger" in config) {
|
|
80
|
+
// Check if config is RouterOptions (has 'routes', 'logger' or 'strict' key)
|
|
81
|
+
if ("routes" in config || "logger" in config || "strict" in config) {
|
|
79
82
|
const options = config;
|
|
80
83
|
routes = options.routes ?? null;
|
|
81
84
|
this.#logger = options.logger ?? null;
|
|
85
|
+
this.#strict = options.strict === true;
|
|
82
86
|
}
|
|
83
87
|
else {
|
|
84
88
|
// Backwards compatible: treat as RouterConfig
|
|
@@ -178,6 +182,7 @@ export class SimpleRouter {
|
|
|
178
182
|
*/
|
|
179
183
|
on(routes, cb, options = {}) {
|
|
180
184
|
const { label = null, allowQueryParams = true } = options;
|
|
185
|
+
const strict = options.strict ?? this.#strict;
|
|
181
186
|
if (!Array.isArray(routes))
|
|
182
187
|
routes = [routes];
|
|
183
188
|
routes.forEach((route) => {
|
|
@@ -185,43 +190,69 @@ export class SimpleRouter {
|
|
|
185
190
|
this.#catchAll = cb;
|
|
186
191
|
}
|
|
187
192
|
else {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
const sr = new SimpleRoute(route, { strict });
|
|
194
|
+
// Dev-only shadowing warning: if an earlier route would consume the
|
|
195
|
+
// literal form of this one, the new route is unreachable. Only runs
|
|
196
|
+
// when debug logging is enabled to avoid per-registration overhead
|
|
197
|
+
// in production.
|
|
198
|
+
if (SimpleRouter.debug)
|
|
199
|
+
this.#warnIfShadowed(route);
|
|
200
|
+
this.#routes.push([sr, cb, allowQueryParams, label]);
|
|
194
201
|
}
|
|
195
202
|
});
|
|
196
203
|
}
|
|
204
|
+
#warnIfShadowed(route) {
|
|
205
|
+
for (const [existing] of this.#routes) {
|
|
206
|
+
if (existing.route === route)
|
|
207
|
+
continue;
|
|
208
|
+
// If the existing pattern matches the literal text of the new route,
|
|
209
|
+
// any real input intended for the new route will also match the
|
|
210
|
+
// existing one first (first-match-wins).
|
|
211
|
+
let matched = null;
|
|
212
|
+
try {
|
|
213
|
+
matched = existing.parse(route);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// ignore — some patterns may not be parseable as inputs
|
|
217
|
+
}
|
|
218
|
+
if (matched !== null) {
|
|
219
|
+
const msg = `[SimpleRouter] route '${route}' is shadowed by earlier ` +
|
|
220
|
+
`route '${existing.route}' (first-match-wins). ` +
|
|
221
|
+
`Register the more specific route first.`;
|
|
222
|
+
if (this.#logger)
|
|
223
|
+
this.#logger.warn(msg);
|
|
224
|
+
else
|
|
225
|
+
console.warn(msg);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
197
230
|
/**
|
|
198
231
|
* Executes pattern matching against the provided string.
|
|
199
|
-
* Returns the value returned by the matched route's callback.
|
|
232
|
+
* Returns the value returned by the matched route's callback (type `T`).
|
|
200
233
|
* Routes are tested in registration order - first match wins.
|
|
201
234
|
*
|
|
202
235
|
* @param url - String to match against registered patterns (can be a URL, file path, command, etc.)
|
|
203
236
|
* @param fallbackFn - Optional fallback function if no route matches
|
|
204
|
-
* @returns The value returned by the matched callback, or false if no match
|
|
237
|
+
* @returns `T | false` - The value returned by the matched callback, or `false` if no match
|
|
205
238
|
*
|
|
206
239
|
* @example
|
|
207
240
|
* ```ts
|
|
208
|
-
* //
|
|
209
|
-
* router
|
|
241
|
+
* // Type-safe routing
|
|
242
|
+
* const router = new SimpleRouter<Component>({ "/home": () => HomePage });
|
|
243
|
+
* const result = router.exec("/home"); // Component | false
|
|
210
244
|
*
|
|
211
245
|
* // With query parameters
|
|
212
246
|
* router.exec("/users?sort=name");
|
|
213
247
|
*
|
|
214
|
-
* // File path matching
|
|
215
|
-
* router.exec("src/components/Button.ts");
|
|
216
|
-
*
|
|
217
|
-
* // Command matching
|
|
218
|
-
* router.exec("user:delete:123");
|
|
219
|
-
*
|
|
220
248
|
* // With fallback
|
|
221
|
-
* router.exec("/unknown", () =>
|
|
249
|
+
* router.exec("/unknown", () => NotFoundPage);
|
|
222
250
|
*
|
|
223
|
-
* //
|
|
224
|
-
* const component = router.exec("/
|
|
251
|
+
* // Check for no match
|
|
252
|
+
* const component = router.exec("/path");
|
|
253
|
+
* if (component !== false) {
|
|
254
|
+
* render(component);
|
|
255
|
+
* }
|
|
225
256
|
* ```
|
|
226
257
|
*/
|
|
227
258
|
exec(url, fallbackFn) {
|
|
@@ -233,7 +264,7 @@ export class SimpleRouter {
|
|
|
233
264
|
if (params !== null) {
|
|
234
265
|
this.#publishCurrent(route.route, params, label);
|
|
235
266
|
this.#dbg(`${dbgPrefix}matches '${route.route}' with`, params);
|
|
236
|
-
return
|
|
267
|
+
return cb(params, route.route);
|
|
237
268
|
}
|
|
238
269
|
}
|
|
239
270
|
if (typeof fallbackFn === "function") {
|
package/package.json
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/simple-router",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/mod.js",
|
|
6
6
|
"types": "dist/mod.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/mod.d.ts",
|
|
10
|
+
"import": "./dist/mod.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md",
|
|
17
|
+
"API.md",
|
|
18
|
+
"AGENTS.md",
|
|
19
|
+
"CLAUDE.md"
|
|
20
|
+
],
|
|
7
21
|
"author": "Marian Meres",
|
|
8
22
|
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@marianmeres/pubsub": "^3.0.0"
|
|
25
|
+
},
|
|
9
26
|
"repository": {
|
|
10
27
|
"type": "git",
|
|
11
28
|
"url": "git+https://github.com/marianmeres/simple-router.git"
|
|
12
29
|
},
|
|
13
30
|
"bugs": {
|
|
14
31
|
"url": "https://github.com/marianmeres/simple-router/issues"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"@marianmeres/pubsub": "^2.2.0",
|
|
18
|
-
"@marianmeres/clog": "^2.3.3"
|
|
19
32
|
}
|
|
20
|
-
}
|
|
33
|
+
}
|