@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 ADDED
@@ -0,0 +1,399 @@
1
+ # @marianmeres/simple-router - AI Agent Reference
2
+
3
+ Machine-readable documentation for AI coding assistants.
4
+
5
+ ## Package Metadata
6
+
7
+ ```yaml
8
+ name: "@marianmeres/simple-router"
9
+ version: "3.2.0"
10
+ runtime: ["deno", "node"]
11
+ module_system: "ESM"
12
+ entry_point: "src/mod.ts"
13
+ npm_dist: ".npm-dist/"
14
+ license: "MIT"
15
+ ```
16
+
17
+ ## Core Concepts
18
+
19
+ ```yaml
20
+ purpose: "Framework-agnostic string pattern matcher and router"
21
+ use_cases:
22
+ - "SPA client-side routing"
23
+ - "File path matching"
24
+ - "Command pattern matching"
25
+ - "Any string identifier matching"
26
+ key_features:
27
+ - "Dynamic route parameters with optional regex constraints"
28
+ - "Wildcard and catch-all routes"
29
+ - "Spread parameters for multi-segment matching"
30
+ - "Query string parsing (toggleable per route)"
31
+ - "Reactive subscriptions (Svelte store compatible)"
32
+ - "First-match-wins routing"
33
+ ```
34
+
35
+ ## File Structure
36
+
37
+ ```yaml
38
+ source_files:
39
+ - path: "src/mod.ts"
40
+ purpose: "Main entry point, re-exports public API"
41
+ exports: ["SimpleRoute", "SimpleRouter", "RouteConfig", "RouteParams", "SimpleRouteOptions", "Logger", "RouteCallback", "RouterConfig", "RouterCurrent", "RouterOnOptions", "RouterOptions", "RouterSubscriber", "RouterUnsubscribe"]
42
+
43
+ - path: "src/route.ts"
44
+ purpose: "Low-level pattern parser"
45
+ exports: ["SimpleRoute", "RouteConfig", "RouteParams", "SimpleRouteOptions"]
46
+ class: "SimpleRoute"
47
+
48
+ - path: "src/router.ts"
49
+ purpose: "High-level router with pub/sub"
50
+ exports: ["SimpleRouter", "RouteParams", "Logger", "RouteCallback", "RouterConfig", "RouterCurrent", "RouterOnOptions", "RouterOptions", "RouterSubscriber", "RouterUnsubscribe"]
51
+ class: "SimpleRouter"
52
+
53
+ test_files:
54
+ - path: "tests/route.test.ts"
55
+ coverage: "Pattern matching, query params, regex-with-parens constraints, strict mode, optional-position rules"
56
+
57
+ - path: "tests/router.test.ts"
58
+ coverage: "Router lifecycle, subscriptions, logger, strict propagation, specificity-shadowing warning"
59
+
60
+ build_files:
61
+ - path: "scripts/build-npm.ts"
62
+ purpose: "Deno-to-npm build script"
63
+ ```
64
+
65
+ ## Dependencies
66
+
67
+ ```yaml
68
+ runtime:
69
+ - name: "@marianmeres/pubsub"
70
+ version: "^2.4.1"
71
+ registry: "jsr"
72
+ purpose: "Reactive subscriptions"
73
+
74
+ dev_only:
75
+ - "@std/assert"
76
+ - "@std/fs"
77
+ - "@std/path"
78
+ ```
79
+
80
+ ## API Summary
81
+
82
+ ### SimpleRouter
83
+
84
+ ```yaml
85
+ class: "SimpleRouter<T = unknown>"
86
+ purpose: "Main router for pattern matching and state management"
87
+ generic_param:
88
+ name: "T"
89
+ default: "unknown"
90
+ description: "Return type of route callbacks and exec() method"
91
+
92
+ static_properties:
93
+ - name: "debug"
94
+ type: "boolean"
95
+ default: false
96
+ description: "Enable debug logging (uses logger if provided, else console.log)"
97
+
98
+ constructor:
99
+ params:
100
+ - name: "config"
101
+ type: "RouterConfig<T> | RouterOptions<T> | null"
102
+ optional: true
103
+ description: "Route config or options object with routes, logger, and strict"
104
+
105
+ methods:
106
+ - name: "on"
107
+ signature: "(routes: string | string[], cb: RouteCallback<T>, options?: RouterOnOptions) => void"
108
+ description: "Register route(s) with callback"
109
+ important: "Routes matched in registration order - first match wins"
110
+
111
+ - name: "exec"
112
+ signature: "(url: string, fallbackFn?: RouteCallback<T>) => T | false"
113
+ description: "Execute pattern matching"
114
+ returns: "Callback result (type T), or false if no match"
115
+
116
+ - name: "subscribe"
117
+ signature: "(subscription: RouterSubscriber) => RouterUnsubscribe"
118
+ description: "Subscribe to state changes (Svelte store compatible)"
119
+ behavior: "Called immediately with current state"
120
+ returns: "Unsubscribe function directly (not wrapped object)"
121
+
122
+ - name: "reset"
123
+ signature: "() => this"
124
+ description: "Clear all routes (except catch-all)"
125
+
126
+ - name: "info"
127
+ signature: "() => Record<string, string>"
128
+ description: "Get route-to-label mapping"
129
+
130
+ properties:
131
+ - name: "current"
132
+ type: "RouterCurrent"
133
+ readonly: true
134
+ description: "Current router state"
135
+ ```
136
+
137
+ ### SimpleRoute
138
+
139
+ ```yaml
140
+ class: SimpleRoute
141
+ purpose: "Low-level pattern parser"
142
+
143
+ static_properties:
144
+ - name: "SPLITTER"
145
+ type: "string"
146
+ default: "/"
147
+ - name: "WILDCARD"
148
+ type: "string"
149
+ default: "*"
150
+
151
+ constructor:
152
+ params:
153
+ - name: "route"
154
+ type: "string"
155
+ description: "Pattern string"
156
+ - name: "options"
157
+ type: "SimpleRouteOptions"
158
+ optional: true
159
+ description: "e.g. { strict: true } to disable empty-segment collapsing"
160
+ throws: "Error if pattern invalid (multiple spread segments, invalid regex, invalid param name, optional-followed-by-required)"
161
+
162
+ static_methods:
163
+ - name: "parseQueryString"
164
+ signature: "(str: string) => RouteParams"
165
+ description: "Parse query string to object"
166
+
167
+ methods:
168
+ - name: "parse"
169
+ signature: "(url: string, allowQueryParams?: boolean) => RouteParams | null"
170
+ description: "Match string against pattern"
171
+ returns: "Params object or null"
172
+ ```
173
+
174
+ ## Pattern Syntax Reference
175
+
176
+ ```yaml
177
+ patterns:
178
+ exact:
179
+ syntax: "literal"
180
+ example: "foo"
181
+ matches: "foo"
182
+
183
+ named_param:
184
+ syntax: "[name]"
185
+ example: "[id]"
186
+ captures: "{ id: 'value' }"
187
+
188
+ regex_param:
189
+ syntax: "[name(regex)]"
190
+ example: "[id([0-9]+)]"
191
+ captures: "{ id: '123' }"
192
+ constraint: "name must match /^\\w+$/. Regex may contain parentheses (capturing/non-capturing groups, alternation) — balanced-paren splitting is used."
193
+
194
+ optional:
195
+ syntax: "[name]?"
196
+ example: "[id]?"
197
+ captures: "{} or { id: 'value' }"
198
+ constraint: "Only allowed in trailing position (or directly before wildcard). Optional-followed-by-required throws at construction."
199
+
200
+ spread:
201
+ syntax: "[...name]"
202
+ example: "[...path]"
203
+ captures: "{ path: 'a/b/c' }"
204
+ constraint: "Only one per route, cannot be optional"
205
+
206
+ wildcard:
207
+ syntax: "*"
208
+ example: "/foo/*"
209
+ constraint: "Must be last segment"
210
+ captures: "{}"
211
+ ```
212
+
213
+ ## Type Definitions
214
+
215
+ ```typescript
216
+ type RouteParams = Record<string, any>;
217
+
218
+ // Generic callback type - T is the return type
219
+ type RouteCallback<T = unknown> = (params: RouteParams | null, route: string) => T;
220
+
221
+ // Generic config type - T is the return type of all callbacks
222
+ type RouterConfig<T = unknown> = Record<string, RouteCallback<T>>;
223
+
224
+ interface Logger {
225
+ debug: (...args: unknown[]) => unknown;
226
+ log: (...args: unknown[]) => unknown;
227
+ warn: (...args: unknown[]) => unknown;
228
+ error: (...args: unknown[]) => unknown;
229
+ }
230
+
231
+ // Generic options type - T is the return type of route callbacks
232
+ interface RouterOptions<T = unknown> {
233
+ routes?: RouterConfig<T> | null;
234
+ logger?: Logger | null;
235
+ strict?: boolean; // propagates to every registered route
236
+ }
237
+
238
+ interface RouterCurrent {
239
+ route: string | null;
240
+ params: RouteParams | null;
241
+ label: string | null;
242
+ }
243
+
244
+ interface RouterOnOptions {
245
+ label?: string | null;
246
+ allowQueryParams?: boolean;
247
+ strict?: boolean; // overrides router-level default
248
+ }
249
+
250
+ interface SimpleRouteOptions {
251
+ strict?: boolean; // disables empty-segment collapsing
252
+ }
253
+
254
+ type RouterSubscriber = (current: RouterCurrent) => void;
255
+
256
+ type RouterUnsubscribe = () => void;
257
+
258
+ interface RouteConfig {
259
+ segment: string;
260
+ name: string | null;
261
+ test: RegExp;
262
+ isOptional: boolean;
263
+ isSpread: boolean;
264
+ }
265
+ ```
266
+
267
+ ## Common Usage Patterns
268
+
269
+ ### SPA Hash Routing (Typed)
270
+
271
+ ```typescript
272
+ // Type-safe router - exec() returns Component | false
273
+ const router = new SimpleRouter<Component>({
274
+ "/": () => HomePage,
275
+ "/user/[id]": (params) => UserPage(params?.id),
276
+ "*": () => NotFoundPage
277
+ });
278
+
279
+ window.addEventListener("hashchange", () => {
280
+ const path = location.hash.slice(1) || "/";
281
+ const result = router.exec(path); // Component | false
282
+ if (result !== false) render(result);
283
+ });
284
+ ```
285
+
286
+ ### File Path Matching
287
+
288
+ ```typescript
289
+ const router = new SimpleRouter({
290
+ "src/[module]/[file].ts": ({ module, file }) => process(module, file),
291
+ "assets/[...path]": ({ path }) => loadAsset(path)
292
+ });
293
+ ```
294
+
295
+ ### Command Routing
296
+
297
+ ```typescript
298
+ const router = new SimpleRouter({
299
+ "user:create": () => createUser(),
300
+ "user:delete:[id]": ({ id }) => deleteUser(id)
301
+ });
302
+ ```
303
+
304
+ ## Important Behaviors
305
+
306
+ ```yaml
307
+ behaviors:
308
+ - name: "First Match Wins"
309
+ description: "Routes matched in registration order"
310
+ implication: "Register specific routes before generic ones. When SimpleRouter.debug is true, shadowing is detected at registration and warned via the logger (or console.warn)."
311
+
312
+ - name: "Separator Normalization"
313
+ description: "Non-strict (default): multiple separators are collapsed and trimmed from ends. Strict (opt-in): internal empty segments are preserved."
314
+ example: "non-strict: '//foo//bar//' matches 'foo/bar'. strict: '/a//b' does NOT match '/a/b'."
315
+
316
+ - name: "Optional Segment Position"
317
+ description: "Optional segments ('[x]?') may only appear trailing (or immediately before wildcard '*'). Optional-followed-by-required throws at construction — register separate routes instead."
318
+
319
+ - name: "Query String Priority"
320
+ description: "Path params override same-name query params"
321
+
322
+ - name: "Catch-all Stored Separately"
323
+ description: "'*' route stored in #catchAll, not in #routes array"
324
+
325
+ - name: "Immediate Subscription Call"
326
+ description: "subscribe() calls callback immediately with current state"
327
+
328
+ - name: "Parameter Names Are Literal"
329
+ description: "Parameter names in the pattern are NOT URI-decoded. The pattern '[id%20x]' produces the key 'id%20x'. Parameter values ARE decoded."
330
+ ```
331
+
332
+ ## Error Conditions
333
+
334
+ ```yaml
335
+ errors:
336
+ - condition: "Multiple spread segments"
337
+ message: "Multiple spread segments are invalid"
338
+
339
+ - condition: "Optional spread"
340
+ message: "Spread segment must not be marked as optional"
341
+
342
+ - condition: "Wildcard not last"
343
+ message: "Wildcard '*' can be used only as a last segment"
344
+
345
+ - condition: "Invalid regex in pattern"
346
+ message: "Invalid regex in route pattern '{segment}': {error}"
347
+
348
+ - condition: "Invalid parameter name in named constraint"
349
+ message: "Invalid parameter name '{name}' in route pattern '{segment}' (must match /^\\w+$/)"
350
+
351
+ - condition: "Optional segment followed by a required segment"
352
+ message: "Invalid route '{route}': optional segment '{segment}?' must not be followed by a required segment ..."
353
+
354
+ - condition: "Non-function subscriber"
355
+ throws: "TypeError: Subscription is not a function"
356
+ ```
357
+
358
+ ## Development Commands
359
+
360
+ ```yaml
361
+ commands:
362
+ test: "deno task test"
363
+ build_npm: "deno task npm:build"
364
+ publish_npm: "deno task npm:publish"
365
+ ```
366
+
367
+ ## Code Modification Guidelines
368
+
369
+ ```yaml
370
+ guidelines:
371
+ - "Maintain Svelte store contract compatibility for subscribe()"
372
+ - "subscribe() returns unsubscribe function directly (not wrapped object)"
373
+ - "First-match-wins is core behavior - do not change"
374
+ - "Query param parsing must remain toggleable per-route"
375
+ - "Keep catch-all ('*') handling separate from regular routes"
376
+ - "URL encoding/decoding: param VALUES are decoded; param NAMES are not"
377
+ - "Logger interface must remain compatible with @marianmeres/clog"
378
+ - "Strict mode is opt-in and propagates router → route; per-route can override"
379
+ - "Shadowing warning runs only when SimpleRouter.debug is true (no perf cost otherwise)"
380
+ - "Run `deno task test` before any changes"
381
+ ```
382
+
383
+ ## Breaking Changes (3.2.0)
384
+
385
+ ```yaml
386
+ breaking_changes:
387
+ - id: "optional-middle-rejected"
388
+ description: "Patterns like '/foo/[bar]?/baz' now throw at construction. Previously they parsed but silently treated the optional marker as required. Register two separate routes instead."
389
+
390
+ - id: "param-names-not-decoded"
391
+ description: "Parameter names are taken literally from the pattern. '/foo/[id%20x]' used to produce the key 'id x'; now produces 'id%20x'. Parameter values are still URI-decoded."
392
+
393
+ - id: "named-constraint-name-validated"
394
+ description: "In a named regex constraint '[name(regex)]', 'name' must match /^\\w+$/. Previously this was not validated."
395
+
396
+ bug_fixes:
397
+ - id: "regex-constraint-with-parens"
398
+ description: "Constraints containing parentheses (e.g. '[s((?:a|b)+)]', '[x((\\d+)-(\\d+))]') used to produce garbage regexes or throw obscure errors due to greedy regex-based splitting. Now uses balanced-paren splitting and works correctly."
399
+ ```