@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/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
- * // Simple config (backwards compatible)
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 'logger' key)
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
- this.#routes.push([
189
- new SimpleRoute(route),
190
- cb,
191
- allowQueryParams,
192
- label,
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
- * // URL matching
209
- * router.exec("/users"); // Returns result of callback
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", () => console.log("Not found"));
249
+ * router.exec("/unknown", () => NotFoundPage);
222
250
  *
223
- * // Can return components, values, etc.
224
- * const component = router.exec("/home");
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 typeof cb === "function" ? cb(params, route.route) : true;
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.1",
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
+ }