white-label-router 6.1.2 → 6.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/README.md CHANGED
@@ -88,7 +88,18 @@ Public navigation should remain a real link:
88
88
  <a href="/products/42" data-pushstate>View product 42</a>
89
89
  ```
90
90
 
91
- `data-pushstate` opts the link into client-side enhancement. Links without it retain native behavior. Modified clicks, non-left clicks, downloads, alternate targets, and cross-origin URLs also remain native.
91
+ `data-pushstate` opts the link into client-side enhancement. Router intercepts a click on an `<a href data-pushstate>` (HTML or SVG; Router reads the `href` attribute, resolved against `document.baseURI`) only when it can actually handle it. These clicks stay native:
92
+
93
+ - links without `data-pushstate`, or clicks whose default action was already prevented;
94
+ - non-left clicks and clicks with Meta, Ctrl, Shift, or Alt held;
95
+ - links with `download`, or a `target` other than `_self`;
96
+ - links to a `#fragment` of the page already shown (same path and query), which the browser handles as in-page fragment navigation; the resulting `popstate` follows the [hash-only rule](#browser-history-behavior);
97
+ - cross-origin or unparseable URLs;
98
+ - URLs that match no route, or only the placeholder `defaultRoute` the constructor installs (a `defaultRoute` you configure yourself does intercept unmatched links);
99
+ - routes with no runnable view behavior;
100
+ - links whose guard returns anything but `true`, or throws (the guard's error still propagates).
101
+
102
+ Once a route has been committed, the click's default action is cancelled even if the route's view throws, so the browser does not navigate a second time.
92
103
 
93
104
  This preserves keyboard behavior, context menus, no-JavaScript navigation, and crawler discovery. Public routes should have directly requestable URLs that return meaningful HTML and appropriate title, description, canonical, and robots metadata.
94
105
 
@@ -111,6 +122,8 @@ Only click events that reach `navigationRoot` are considered by that Router. Lin
111
122
 
112
123
  `navigationRoot` is deliberately separate from `scope`: `navigationRoot` controls where enhanced-link clicks are observed; `scope` is application data passed to route callbacks.
113
124
 
125
+ `navigationRoot` does not limit focus. After a navigation, the route's `focus` selector (default `main h1`) is looked up in the whole document, so a heading outside the owned region still receives focus. Use a more specific `focus` selector, or `focus: false`, when the region should keep focus itself.
126
+
114
127
  ## Define routes
115
128
 
116
129
  A route can be a function:
@@ -145,7 +158,28 @@ router.routes = {
145
158
  };
146
159
  ```
147
160
 
148
- `secure()` must return exactly `true` to allow navigation. Client guards are interface behavior, not server authorization. Router validates the destination guard and runnable view behavior before tearing down the current route. On a successful route change, the previous route's `destroy()` runs before the next route's `initialize()`. Router snapshots the destroy callback that belongs to the lifecycle that actually initialized, so later replacement or mutation of the route table does not retarget teardown. The destroy callback receives the incoming location as `nextLocation`. If browser title/focus work throws after a route has initialized, that initialized lifecycle remains owned and is still torn down before a later successful route starts.
161
+ ### Guards
162
+
163
+ `secure(scope, location)` runs synchronously with the destination location, before anything is torn down. It must return exactly `true` to allow navigation; any other value (including a Promise) rejects it and `navigate()` returns `false`. An error thrown by a guard propagates to the caller and leaves Router state unchanged. Client guards are interface behavior, not server authorization.
164
+
165
+ ### Lifecycle order
166
+
167
+ A successful navigation runs these steps in order:
168
+
169
+ 1. Match the route and run its guard. Rejection stops here with no state change.
170
+ 2. Commit `router.url`, `router.route`, and `router.locationData` for the destination.
171
+ 3. Call the previous route's `destroy(scope, nextLocation)`, if it had one.
172
+ 4. Run the new route: a function route, a `view` function, or `view.initialize(scope, location)`.
173
+ 5. Apply the route's title and focus (browser only; see [Title and focus](#title-and-focus)).
174
+ 6. Set `router.previousRoute`, then push a history entry (browser only; skipped for `initialize()` and back/forward).
175
+
176
+ Router keeps the `destroy` callback of the lifecycle that actually initialized, so replacing or mutating the route table later does not change which teardown runs. That lifecycle stays owned even if `initialize()` throws, or title/focus work throws after it: the route remains committed, the error is rethrown to the caller, and its `destroy()` still runs before the next successful route starts. Title, focus, and history are skipped for a route whose `initialize()` threw.
177
+
178
+ If the previous route's `destroy()` throws, its error propagates out of `navigate()` and that teardown is never called again: Router releases it before calling it. The destination stays committed (`router.url`, `router.route`, `router.locationData`), but its view does not run, so it owns no `destroy()`, and title, focus, `previousRoute`, and history are skipped. The next navigation therefore starts without a teardown; `navigate()` with no URL re-runs the committed destination to initialize it.
179
+
180
+ ### Navigating from inside a route
181
+
182
+ `navigate()` is re-entrant. If `initialize()`, `destroy()`, a focus handler, or a mediator event starts another navigation, the newer navigation wins: the outer one stops at that point, applies no title, focus, or history of its own, and returns the router. For example, navigating to `/two` from inside `/one`'s `initialize()` runs `/one`'s `destroy()` and then `/two`'s `initialize()`, and leaves `/two` as the active route.
149
183
 
150
184
  ## Matching
151
185
 
@@ -163,6 +197,22 @@ router.routes = {
163
197
 
164
198
  `/products/sale` selects `saleRoute` even though `/products` was defined first. When no prefix matches, `defaultRoute` runs if configured.
165
199
 
200
+ Route keys are encoded the same way URL parsing encodes paths, so `'/café'` matches a request for `/caf%C3%A9`. Keys that don't start with `/`, or that contain `?` or `#`, can never match a pathname and are ignored (apart from `defaultRoute`). If two keys normalize to the same path (for example `/x/../same` and `/same`), the key already in normalized form wins, otherwise the alphabetically lowest; insertion order never decides.
201
+
202
+ A trailing `/` is part of the key, not a wildcard. `'/docs'` matches `/docs`, `/docs/`, and `/docs/x`, but `'/docs/'` matches only `/docs/` itself (and paths below its empty last segment, such as `/docs//x`). It doesn't match `/docs` or `/docs/x`. Declare `'/docs'` to cover the section. If both keys exist, `/docs/` selects `'/docs/'` as the longer match. `'/'` follows the same rule: it matches only the root path, so other unmatched paths go to `defaultRoute`.
203
+
204
+ ### Large route tables
205
+
206
+ Each match reads the keys of `routes` again so that keys added or removed in place (`router.routes['/x'] = fn`) take effect immediately. That costs time in proportion to the number of keys: about 50 µs per match at 1,000 routes in `npm run benchmark`. For a large table that doesn't change after setup, call `router.indexRoutes()` once the table is ready. Matching then uses the stored index (about 0.15 µs at 1,000 routes) until `routes` is assigned a different object, which drops the index. After adding or removing keys on the same object, call `indexRoutes()` again. Until you do, an added key is not matched, and navigation to a removed key's paths returns `false`. Replacing the value of an existing key needs no re-index.
207
+
208
+ ```js
209
+ router.routes = buildRouteTable(); // thousands of keys
210
+ router.indexRoutes();
211
+ router.initialize();
212
+ ```
213
+
214
+ A new `Router` starts with a placeholder `defaultRoute` that does nothing, so programmatic navigation to an unmatched URL succeeds until you replace `routes`. That placeholder never intercepts link clicks.
215
+
166
216
  ## Location contract
167
217
 
168
218
  Routes receive the configured `scope` and a location object:
@@ -178,7 +228,12 @@ Routes receive the configured `scope` and a location object:
178
228
  }
179
229
  ```
180
230
 
181
- `data.url` contains path segments after the matched prefix, `data.query` contains parsed query values, and `data.mediator` contains navigation data supplied programmatically or through `CustomEvent.detail`. Values are URI-decoded with standard WHATWG URL APIs; applications must still validate untrusted values.
231
+ - `url` is the path, query, and hash (`/products/42?color=blue#reviews`), never a full absolute URL. This holds on the server too: `initialize('https://example.com/products/42?color=blue')` produces `url: '/products/42?color=blue'`. `router.url` holds the same value.
232
+ - `data.url` contains the URI-decoded path segments after the matched route prefix (all segments for `defaultRoute`). A segment with malformed percent-encoding is passed through undecoded.
233
+ - `data.query` contains query values decoded with `URLSearchParams`; for duplicate keys the last value wins.
234
+ - `data.mediator` contains navigation data passed to `navigate()` or through `CustomEvent.detail`. It is `{}` for link clicks, `initialize()`, and back/forward, and `undefined` when `navigate()` is called without data.
235
+
236
+ Applications must still validate these untrusted values.
182
237
 
183
238
  ## Programmatic navigation
184
239
 
@@ -192,7 +247,7 @@ In browsers, successful navigation updates history unless the call represents `p
192
247
 
193
248
  Rejected navigation is atomic from Router's perspective: malformed or wrong-type URL candidates, cross-origin browser URLs, URLs with no matching route/default route, failed guards, and routes without runnable view behavior return `false` without destroying the current route, pushing browser history, or replacing the last successful URL, selected route, or location payload.
194
249
 
195
- Calling `navigate()` without a URL dispatches the current browser URL. In a non-browser runtime the safe default is `/`; server applications should normally supply a fully qualified request URL explicitly.
250
+ Calling `navigate()` without a URL re-runs Router's last committed URL. In a browser, that re-run adds a history entry only when the address bar does not already show that URL, so it never duplicates the current entry. Before anything has been committed it uses the current browser URL, or `/` on a server; server applications should normally supply a fully qualified request URL explicitly. Errors thrown by a guard or a route callback propagate out of `navigate()`.
196
251
 
197
252
  ## Mediator integration
198
253
 
@@ -214,7 +269,7 @@ mediator.dispatchEvent(new CustomEvent('router:navigate', {
214
269
  }));
215
270
  ```
216
271
 
217
- The `CustomEvent.detail` value becomes `location.data.mediator`. `destroy()` removes the listener owned by Router. Repeated listener initialization is idempotent. Reassigning `router.mediator` while listeners are active moves the `router:navigate` subscription to the replacement.
272
+ The `CustomEvent.detail` object becomes `location.data.mediator`, and its `url` (if any) is the destination. A detail without `url`, or a `null`/missing detail, re-runs the current URL without adding a duplicate history entry; a detail that is not a plain object (a string, number, or array) is ignored. Mediator navigations to a `url` push browser history like `navigate()`. `destroy()` removes the listener owned by Router. Repeated listener initialization is idempotent. Reassigning `router.mediator` while listeners are active moves the `router:navigate` subscription to the replacement.
218
273
 
219
274
  Router does not import or require `white-label-mediator`; the integration is structural so another compatible EventTarget can be used instead.
220
275
 
@@ -259,12 +314,14 @@ router.routes = {
259
314
  };
260
315
  ```
261
316
 
262
- The default focus selector is `main h1`. Set `focus: false` for an in-page refinement that should preserve current focus. Server runtimes retain `pageTitle` as route state but do not mutate a document or focus target.
317
+ After a route renders, Router sets `document.title` from `title` and focuses the first element in the document matching `focus` (default `main h1`), regardless of `navigationRoot`, adding `tabindex="-1"` if the element has no `tabindex`. Set `focus: false` for an in-page refinement that should preserve current focus. The first dispatch from `initialize()` sets the title but does not move focus, because the browser already owns focus on a freshly loaded page; later navigations do. Server runtimes retain `pageTitle` as route state but do not mutate a document or focus target.
263
318
 
264
319
  ## Browser history behavior
265
320
 
266
321
  Initialization dispatches the current URL without adding a duplicate history entry. Back/forward navigation reads `window.location`—including path, query, and hash—even when history state is null or belongs to another application. History state is not treated as the authoritative URL. Rejected navigation does not add a history entry.
267
322
 
323
+ When back/forward, or a click on a link to a `#fragment` of the current page, changes only the hash (footnotes, skip links), Router updates `router.url` and `locationData.url` but does not re-run the route, call `destroy()`, or move focus. A change to the path or query re-runs the route as usual.
324
+
268
325
  ## Public API
269
326
 
270
327
  | Member | Behavior | Returns |
@@ -277,12 +334,15 @@ Initialization dispatches the current URL without adding a duplicate history ent
277
334
  | `navigate(url?, data?, isPopState?)` | Match and run a route; update browser history when appropriate. Invalid/malformed URL candidates and other rejected navigation preserve current route state. | The same Router on success; `false` for rejected navigation. |
278
335
  | `addListeners()` | Attach browser and optional mediator listeners once. | The same `Router` instance. |
279
336
  | `removeListeners()` | Release listeners owned by this router. | The same `Router` instance. |
280
- | `destroy()` | Release routing listeners. | The same `Router` instance after cleanup. |
337
+ | `indexRoutes()` | Index the current route keys and reuse that index until `routes` is reassigned or this is called again (see [Large route tables](#large-route-tables)). | The same `Router` instance. |
338
+ | `destroy()` | Release routing listeners. Does not call the active route's `destroy()`. | The same `Router` instance after cleanup. |
281
339
  | `parseQueryString(query)` | Decode a query string using `URLSearchParams`. | A plain object containing the last value for each query key. |
282
340
  | `setLocationData(data?)` | Rebuild the current route location payload. | `undefined`; updates `locationData` in place. |
283
341
  | `applyPageContext(route)` | Update route title/focus context when applicable. | The same `Router` instance. |
284
- | `eventPushStateClick(event)` | Handle an eligible browser push-state click. | The same Router after handled navigation; `true` when native/default handling should continue. |
285
- | `eventPopState()` | Dispatch the current browser history location. | The same `Router` instance. |
342
+ | `eventPushStateClick(event)` | Handle an eligible browser push-state click; same-page `#fragment` links stay native (see [Progressive enhancement first](#progressive-enhancement-first)). | The same Router after handled navigation; `true` when native/default handling should continue. |
343
+ | `eventPopState()` | Dispatch the current browser history location; a hash-only change just updates `url`. | The same `Router` instance. |
344
+
345
+ Router records navigation state on public properties that applications should treat as read-only: `url`, `route`, `previousRoute`, `locationData`, and `pageTitle`.
286
346
 
287
347
  ## TypeScript
288
348
 
@@ -304,17 +364,19 @@ router.initialize();
304
364
 
305
365
  ```sh
306
366
  npm ci --ignore-scripts
307
- npm run lint
308
- npm run typecheck
309
- npm test
310
- npm run coverage
311
- npm run audit
312
- npm pack --dry-run
367
+ npm run lint && npm run coverage && npm run audit
313
368
  ```
314
369
 
315
- Tests cover browser behavior, scoped progressive navigation, DOM-free server routing, atomic rejected navigation, mediator reassignment, and parity of the shared route contract. Coverage enforces 100% statements, branches, functions, and lines per implementation file. CI verifies the documented Node minimum and primary Node line, audits dependencies, packs the package, and verifies the public API across npm, Yarn, and pnpm.
370
+ `npm run coverage` builds (which typechecks), runs the consumer type checks, and runs every Node test, so `npm run typecheck` and `npm test` don't need separate runs. For a quick loop while iterating, use `npm run build && node --test test/<file>.test.js`. `npm run lint` covers `src/`, `test/`, and `benchmark/`. `dist/` is generated and not committed; `npm run build` (also run by `prepare` for git installs) creates it.
371
+
372
+ After `npm run build`:
373
+
374
+ - `npm run test:browser` runs `test/browser/` in real Chromium through Playwright (real link clicks, back/forward, fragments, focus, `navigationRoot`) against pages served by a local `node:http` server. It is not part of `npm test`. Install a browser once with `npx playwright install chromium`, or point `PLAYWRIGHT_CHROMIUM_EXECUTABLE` at an existing Chromium.
375
+ - `npm run benchmark` times `matchRoute`, `navigate`, and link clicks with 10 and 1,000 routes. `npm run bench:ci` is a short version that fails if any operation is more than 3x slower than `benchmark/baseline.json` or a deterministic counter differs. Refresh the baseline after an intended performance change with `node benchmark/router.bench.js --ci --update-baseline` and commit the result.
376
+
377
+ Tests cover browser behavior, scoped progressive navigation, DOM-free server routing, atomic rejected navigation, mediator reassignment, and parity of the shared route contract. Coverage enforces 100% statements, branches, functions, and lines per implementation file. CI verifies the documented Node minimum and primary Node line, runs the short benchmark and the Chromium browser tests, audits dependencies, packs the package, and verifies the public API across npm, Yarn, and pnpm.
316
378
 
317
- Edit `src/*.ts` and regenerate `dist`; do not edit generated files directly.
379
+ Edit `src/index.ts`; `dist` is build output and is not edited or committed. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
318
380
 
319
381
  ## Design boundary
320
382
 
package/dist/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
- /** Navigation payload, including caller-defined metadata. */
1
+ /** Extra data passed with a navigation. `url`, if present, is the destination; any other fields are yours. */
2
2
  interface NavigationData {
3
3
  url?: string;
4
4
  [key: string]: unknown;
5
5
  }
6
- /** URL-derived values supplied to route handlers. */
6
+ /**
7
+ * What a route callback receives about the destination.
8
+ * `url` is the path, query, and hash; `data.url` holds the decoded path segments after the matched route key;
9
+ * `data.query` holds decoded query values; `data.mediator` is the navigation data, if any.
10
+ */
7
11
  interface LocationData {
8
12
  url: string;
9
13
  data: {
@@ -12,12 +16,17 @@ interface LocationData {
12
16
  query: Record<string, string>;
13
17
  };
14
18
  }
19
+ /** A route callback. It receives `router.scope` and the destination location. */
15
20
  type RouteHandler = (scope: Element | null, location: LocationData) => unknown;
21
+ /** A view with setup and teardown. `destroy` receives the location being navigated to. */
16
22
  interface RouteViewLifecycle {
17
23
  initialize?: RouteHandler;
18
24
  destroy?: RouteHandler;
19
25
  }
20
- /** An object route can own a function or a view lifecycle. */
26
+ /**
27
+ * A route with optional page context and a guard.
28
+ * `secure` must return exactly `true` to allow navigation; `focus: false` leaves focus where it is.
29
+ */
21
30
  interface RouteObject {
22
31
  title?: string;
23
32
  focus?: string | false;
@@ -25,12 +34,15 @@ interface RouteObject {
25
34
  view?: RouteHandler | RouteViewLifecycle;
26
35
  }
27
36
  type Route = RouteHandler | RouteObject;
28
- /** Minimal standards-based event target contract required by the router. */
37
+ /** Any EventTarget-like object; Router only adds and removes a `router:navigate` listener on it. */
29
38
  interface NavigationMediator {
30
39
  addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
31
40
  removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
32
41
  }
33
- /** History API router that also dispatches request URLs in non-browser runtimes. */
42
+ /**
43
+ * Maps URLs to route callbacks. In a browser it enhances `data-pushstate` links and back/forward with the History API;
44
+ * on a server it dispatches the request URL you pass, with no DOM required.
45
+ */
34
46
  declare class Router {
35
47
  routes: Record<string, Route>;
36
48
  url: string;
@@ -48,57 +60,125 @@ declare class Router {
48
60
  private currentNavigationRoot;
49
61
  private boundNavigationRoot;
50
62
  private activeDestroy;
51
- /** Create an instance with its own state and listener references. */
63
+ private compiledRoutes;
64
+ private indexedRoutes;
65
+ private navigationToken;
66
+ private initialDispatch;
67
+ private suppressFocus;
68
+ /** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */
52
69
  constructor();
70
+ /** Event target that can send `router:navigate` events, or `false` for none. */
53
71
  get mediator(): NavigationMediator | false;
54
- /** Move an active router:navigate subscription when the mediator changes. */
72
+ /** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */
55
73
  set mediator(value: NavigationMediator | false);
74
+ /** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */
56
75
  get navigationRoot(): Document | Element | null;
57
- /** Scope progressive link interception and move an active click listener when the root changes. */
76
+ /** Set the click root. If listeners are active, the click listener moves to the new root. */
58
77
  set navigationRoot(value: Document | Element | null);
59
- /** True when browser navigation APIs are available. */
78
+ /** True when `window` and `document` exist, i.e. browser-only effects should run. */
60
79
  private isBrowserRuntime;
61
- /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
80
+ /** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */
62
81
  private getOrigin;
63
- /** Read the authoritative browser URL, or retain the supplied server URL. */
82
+ /** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */
64
83
  private getCurrentUrl;
65
- /** Parse a navigation candidate without changing Router state. */
84
+ /** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */
66
85
  private parseNavigationUrl;
67
- /** Normalize browser navigation to a same-origin path, query, and hash. */
86
+ /** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */
68
87
  private normalizeBrowserUrl;
69
- /** Return whether a selected route has runnable view behavior. */
88
+ /** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */
70
89
  private isRunnableRoute;
71
- /** Return the longest matching own route-table prefix, falling back to defaultRoute when configured. */
90
+ /**
91
+ * Index the current route keys and match against that index without re-reading the keys on every navigation.
92
+ * Without it, each match lists the keys of `routes` to notice in-place edits, which costs time in proportion to
93
+ * the number of routes. Use it for large route tables that stop changing after setup. Assigning a different object to
94
+ * `routes` drops the index; after adding or removing keys on the same object, call `indexRoutes()` again. Until then,
95
+ * an added key is not matched, and a removed key still claims its paths, so `navigate()` returns `false` for them
96
+ * as it does for a key set to `undefined`.
97
+ * Replacing the value of an existing key needs no re-index, because the route is looked up when it runs.
98
+ * @returns This router.
99
+ */
100
+ indexRoutes(): this;
101
+ /** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */
102
+ private compileRoutes;
103
+ /** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */
72
104
  private matchRoute;
73
- /** Build decoded path, query, and mediator data for a navigation candidate. */
105
+ /** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */
106
+ private routePrefixLength;
107
+ /** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */
108
+ private parseStoredUrl;
109
+ /** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */
74
110
  private buildLocationData;
75
111
  /**
76
- * Start this instance and return it for lifecycle chaining.
77
- * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
112
+ * Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.
113
+ * @param url - Request URL on a server. In a browser, omit it to use `window.location`.
114
+ * @returns This router.
115
+ * @throws Whatever a guard or route callback throws.
78
116
  */
79
117
  initialize(url?: string): this;
80
- /** Register browser and optional mediator handlers once. */
118
+ /**
119
+ * Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.
120
+ * @returns This router.
121
+ */
81
122
  addListeners(): this;
82
- /** Release owned state and listeners so the instance can leave the application lifecycle. */
123
+ /**
124
+ * Remove this router's listeners. Does not call the active route's `destroy()`.
125
+ * @returns This router.
126
+ */
83
127
  destroy(): this;
84
- /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
128
+ /**
129
+ * Remove the listeners `addListeners()` attached. Subclasses can override this to release more.
130
+ * @returns This router.
131
+ */
85
132
  removeListeners(): this;
86
- /** Intercept eligible same-origin push-state links while preserving normal browser actions. */
133
+ /**
134
+ * Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:
135
+ * a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,
136
+ * runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left
137
+ * to the browser.
138
+ * @param e - The click event.
139
+ * @returns This router if it handled the click, otherwise `true`.
140
+ * @throws Whatever a guard or route callback throws.
141
+ */
87
142
  eventPushStateClick(e: MouseEvent): true | this;
88
- /** Restore browser history state without adding a new history entry. */
143
+ /**
144
+ * Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.
145
+ * A hash-only change just updates `url` and `locationData.url`.
146
+ * @returns This router.
147
+ */
89
148
  eventPopState(): this;
90
- /** Decode URL query parameters, keeping the last value for duplicate keys. */
149
+ /**
150
+ * Decode a query string into a plain object. For repeated keys the last value wins.
151
+ * @param queryString - Query string, with or without the leading `?`.
152
+ */
91
153
  parseQueryString(queryString: string): {
92
154
  [k: string]: string;
93
155
  };
94
- /** Build decoded path, query, and mediator data for the selected route. */
156
+ /**
157
+ * Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.
158
+ * @param mediatorData - Value to expose as `locationData.data.mediator`.
159
+ */
95
160
  setLocationData(mediatorData?: NavigationData): void;
96
- /** Update page context after a route renders. DOM effects are browser-only. */
161
+ /**
162
+ * Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's
163
+ * `focus` selector (default `main h1`), adding `tabindex="-1"` if needed. `focus: false` skips focus.
164
+ * @param route - The route that just ran.
165
+ * @returns This router.
166
+ */
97
167
  applyPageContext(route: Route): this;
98
- /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
168
+ /**
169
+ * Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,
170
+ * run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).
171
+ * If route code starts another navigation part-way through, that one wins and this call stops early.
172
+ * @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).
173
+ * @param mediatorData - Value exposed to the route as `location.data.mediator`.
174
+ * @param isPopState - `true` to skip adding a history entry.
175
+ * @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,
176
+ * guard refused, or nothing to run). A rejected navigation changes nothing.
177
+ * @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.
178
+ */
99
179
  navigate(url?: string, mediatorData?: NavigationData, isPopState?: boolean): false | this;
100
180
  }
101
- /** Public route configuration and navigation payload types. */
181
+ /** Types for route definitions, location data, and navigation data. */
102
182
  declare namespace Router {
103
183
  type Handler = RouteHandler;
104
184
  type Route = RouteHandler | RouteObject;
package/dist/index.js CHANGED
@@ -1,5 +1,25 @@
1
1
  "use strict";
2
- /** History API router that also dispatches request URLs in non-browser runtimes. */
2
+ /** The do-nothing `defaultRoute` objects each constructor installs, so link clicks can tell them apart from app routes. */
3
+ const builtInDefaultRoutes = new WeakSet();
4
+ /** A dummy origin that lets route keys and server URLs go through the standard URL parser. */
5
+ const routeKeyBase = 'http://white-label.invalid';
6
+ /** Encode a route key the way the URL parser encodes paths, so `'/café'` matches `/caf%C3%A9`. */
7
+ function normalizeRouteKey(key) {
8
+ // A key that doesn't start with '/' or contains '?' or '#' can never equal a pathname, so leave it as is (it won't match).
9
+ if (!key.startsWith('/') || /[?#]/.test(key)) {
10
+ return key;
11
+ }
12
+ return new URL(`${routeKeyBase}${key}`).pathname;
13
+ }
14
+ /** Strip the `#fragment`, leaving the path and query. */
15
+ function withoutHash(url) {
16
+ const index = url.indexOf('#');
17
+ return index === -1 ? url : url.slice(0, index);
18
+ }
19
+ /**
20
+ * Maps URLs to route callbacks. In a browser it enhances `data-pushstate` links and back/forward with the History API;
21
+ * on a server it dispatches the request URL you pass, with no DOM required.
22
+ */
3
23
  class Router {
4
24
  routes;
5
25
  url;
@@ -17,16 +37,26 @@ class Router {
17
37
  currentNavigationRoot = null;
18
38
  boundNavigationRoot = null;
19
39
  activeDestroy;
20
- /** Create an instance with its own state and listener references. */
40
+ compiledRoutes;
41
+ // The routes object indexRoutes() indexed. While `routes` is still that object, matching trusts the index as is.
42
+ indexedRoutes;
43
+ // Bumped by each navigation that passes its guard. If it changes while a navigation is calling out to app code,
44
+ // a newer navigation has started, and the older one stops before touching title, focus, or history.
45
+ navigationToken = 0;
46
+ // True only for the first navigate() call made by initialize(), so that dispatch leaves focus alone.
47
+ initialDispatch = false;
48
+ // Tells applyPageContext() to skip focus for the current call.
49
+ suppressFocus = false;
50
+ /** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */
21
51
  constructor() {
22
- this.routes = {
23
- defaultRoute: {
24
- view: {
25
- initialize: () => true,
26
- destroy: () => true
27
- }
52
+ const defaultRoute = {
53
+ view: {
54
+ initialize: () => true,
55
+ destroy: () => true
28
56
  }
29
57
  };
58
+ builtInDefaultRoutes.add(defaultRoute);
59
+ this.routes = { defaultRoute };
30
60
  this.url = '';
31
61
  this.route = null;
32
62
  this.scope = null;
@@ -48,8 +78,9 @@ class Router {
48
78
  };
49
79
  this.listenersInitialized = false;
50
80
  }
81
+ /** Event target that can send `router:navigate` events, or `false` for none. */
51
82
  get mediator() { return this.currentMediator; }
52
- /** Move an active router:navigate subscription when the mediator changes. */
83
+ /** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */
53
84
  set mediator(value) {
54
85
  if (value === this.currentMediator) {
55
86
  return;
@@ -64,8 +95,9 @@ class Router {
64
95
  this.boundMediator = value;
65
96
  }
66
97
  }
98
+ /** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */
67
99
  get navigationRoot() { return this.currentNavigationRoot; }
68
- /** Scope progressive link interception and move an active click listener when the root changes. */
100
+ /** Set the click root. If listeners are active, the click listener moves to the new root. */
69
101
  set navigationRoot(value) {
70
102
  if (value === this.currentNavigationRoot) {
71
103
  return;
@@ -80,22 +112,22 @@ class Router {
80
112
  }
81
113
  this.currentNavigationRoot = value;
82
114
  }
83
- /** True when browser navigation APIs are available. */
115
+ /** True when `window` and `document` exist, i.e. browser-only effects should run. */
84
116
  isBrowserRuntime() {
85
117
  return typeof window !== 'undefined' && typeof document !== 'undefined';
86
118
  }
87
- /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
119
+ /** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */
88
120
  getOrigin() {
89
121
  return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';
90
122
  }
91
- /** Read the authoritative browser URL, or retain the supplied server URL. */
123
+ /** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */
92
124
  getCurrentUrl() {
93
125
  if (!this.isBrowserRuntime()) {
94
126
  return this.url || '/';
95
127
  }
96
128
  return window.location.pathname + window.location.search + (window.location.hash || '');
97
129
  }
98
- /** Parse a navigation candidate without changing Router state. */
130
+ /** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */
99
131
  parseNavigationUrl(url) {
100
132
  try {
101
133
  const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();
@@ -105,7 +137,7 @@ class Router {
105
137
  return null;
106
138
  }
107
139
  }
108
- /** Normalize browser navigation to a same-origin path, query, and hash. */
140
+ /** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */
109
141
  normalizeBrowserUrl(url) {
110
142
  if (!this.isBrowserRuntime()) {
111
143
  return url;
@@ -116,35 +148,101 @@ class Router {
116
148
  }
117
149
  return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
118
150
  }
119
- /** Return whether a selected route has runnable view behavior. */
151
+ /** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */
120
152
  isRunnableRoute(route) {
121
153
  return typeof route === 'function' || typeof route.view === 'function' ||
122
154
  Boolean(route.view && typeof route.view.initialize === 'function');
123
155
  }
124
- /** Return the longest matching own route-table prefix, falling back to defaultRoute when configured. */
125
- matchRoute(pathname) {
126
- let candidateRoute = null;
127
- for (const route of Object.keys(this.routes)) {
128
- if (route === 'defaultRoute' || (pathname !== route && !pathname.startsWith(`${route}/`))) {
156
+ /**
157
+ * Index the current route keys and match against that index without re-reading the keys on every navigation.
158
+ * Without it, each match lists the keys of `routes` to notice in-place edits, which costs time in proportion to
159
+ * the number of routes. Use it for large route tables that stop changing after setup. Assigning a different object to
160
+ * `routes` drops the index; after adding or removing keys on the same object, call `indexRoutes()` again. Until then,
161
+ * an added key is not matched, and a removed key still claims its paths, so `navigate()` returns `false` for them
162
+ * as it does for a key set to `undefined`.
163
+ * Replacing the value of an existing key needs no re-index, because the route is looked up when it runs.
164
+ * @returns This router.
165
+ */
166
+ indexRoutes() {
167
+ this.indexedRoutes = undefined;
168
+ this.compileRoutes();
169
+ this.indexedRoutes = this.routes;
170
+ return this;
171
+ }
172
+ /** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */
173
+ compileRoutes() {
174
+ const cached = this.compiledRoutes;
175
+ if (cached && this.indexedRoutes === this.routes) {
176
+ return cached;
177
+ }
178
+ this.indexedRoutes = undefined;
179
+ const keys = Object.keys(this.routes);
180
+ if (cached && cached.keys.length === keys.length && cached.keys.every((key, index) => key === keys[index])) {
181
+ return cached;
182
+ }
183
+ const prefixes = new Map();
184
+ const prefixFor = new Map();
185
+ for (const key of keys) {
186
+ if (key === 'defaultRoute') {
129
187
  continue;
130
188
  }
131
- if (!candidateRoute || route.length > candidateRoute.length) {
132
- candidateRoute = route;
189
+ const prefix = normalizeRouteKey(key);
190
+ prefixFor.set(key, prefix);
191
+ const existing = prefixes.get(prefix);
192
+ // When two keys normalize to the same path, prefer the one already in normalized form, then the alphabetically
193
+ // lowest, so the winner never depends on insertion order.
194
+ if (existing === undefined || (existing !== prefix && (key === prefix || key < existing))) {
195
+ prefixes.set(prefix, key);
133
196
  }
134
197
  }
135
- if (!candidateRoute && Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {
198
+ this.compiledRoutes = { keys, prefixes, prefixFor };
199
+ return this.compiledRoutes;
200
+ }
201
+ /** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */
202
+ matchRoute(pathname) {
203
+ const { prefixes } = this.compileRoutes();
204
+ // Try the full path, then drop one '/segment' at a time; the first hit is the longest match.
205
+ // Cutting only at '/' means '/products' matches '/products/42' but not '/products-old'.
206
+ let candidateRoute = prefixes.get(pathname);
207
+ let index = pathname.lastIndexOf('/');
208
+ while (candidateRoute === undefined && index >= 0) {
209
+ candidateRoute = prefixes.get(pathname.slice(0, index));
210
+ index = index > 0 ? pathname.lastIndexOf('/', index - 1) : -1;
211
+ }
212
+ if (candidateRoute !== undefined) {
213
+ return candidateRoute;
214
+ }
215
+ if (Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {
136
216
  return 'defaultRoute';
137
217
  }
138
- return candidateRoute;
218
+ return null;
139
219
  }
140
- /** Build decoded path, query, and mediator data for a navigation candidate. */
220
+ /** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */
221
+ routePrefixLength(route) {
222
+ if (!route || route === 'defaultRoute') {
223
+ return 0;
224
+ }
225
+ return (this.compileRoutes().prefixFor.get(route) ?? normalizeRouteKey(route)).length;
226
+ }
227
+ /** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */
228
+ parseStoredUrl(url) {
229
+ if (!url.startsWith('/')) {
230
+ return this.parseNavigationUrl(url);
231
+ }
232
+ try {
233
+ return new URL(`${this.getOrigin()}${url}`);
234
+ }
235
+ catch (_error) {
236
+ return null;
237
+ }
238
+ }
239
+ /** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */
141
240
  buildLocationData(url, route, mediatorData, parsedUrl) {
142
- const matchedRoute = route && route !== 'defaultRoute' ? route : '';
143
241
  return {
144
242
  url,
145
243
  data: {
146
244
  url: parsedUrl.pathname
147
- .slice(matchedRoute.length)
245
+ .slice(this.routePrefixLength(route))
148
246
  .split('/')
149
247
  .filter(Boolean)
150
248
  .map((fragment) => {
@@ -161,15 +259,27 @@ class Router {
161
259
  };
162
260
  }
163
261
  /**
164
- * Start this instance and return it for lifecycle chaining.
165
- * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
262
+ * Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.
263
+ * @param url - Request URL on a server. In a browser, omit it to use `window.location`.
264
+ * @returns This router.
265
+ * @throws Whatever a guard or route callback throws.
166
266
  */
167
267
  initialize(url) {
168
268
  this.addListeners();
169
- this.navigate(url ?? this.getCurrentUrl(), {}, true);
269
+ // The browser has already placed focus on the page it just loaded, so only later navigations move it.
270
+ this.initialDispatch = true;
271
+ try {
272
+ this.navigate(url ?? this.getCurrentUrl(), {}, true);
273
+ }
274
+ finally {
275
+ this.initialDispatch = false;
276
+ }
170
277
  return this;
171
278
  }
172
- /** Register browser and optional mediator handlers once. */
279
+ /**
280
+ * Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.
281
+ * @returns This router.
282
+ */
173
283
  addListeners() {
174
284
  if (this.listenersInitialized) {
175
285
  return this;
@@ -187,12 +297,18 @@ class Router {
187
297
  this.listenersInitialized = true;
188
298
  return this;
189
299
  }
190
- /** Release owned state and listeners so the instance can leave the application lifecycle. */
300
+ /**
301
+ * Remove this router's listeners. Does not call the active route's `destroy()`.
302
+ * @returns This router.
303
+ */
191
304
  destroy() {
192
305
  this.removeListeners();
193
306
  return this;
194
307
  }
195
- /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
308
+ /**
309
+ * Remove the listeners `addListeners()` attached. Subclasses can override this to release more.
310
+ * @returns This router.
311
+ */
196
312
  removeListeners() {
197
313
  if (!this.listenersInitialized) {
198
314
  return this;
@@ -209,7 +325,15 @@ class Router {
209
325
  this.listenersInitialized = false;
210
326
  return this;
211
327
  }
212
- /** Intercept eligible same-origin push-state links while preserving normal browser actions. */
328
+ /**
329
+ * Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:
330
+ * a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,
331
+ * runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left
332
+ * to the browser.
333
+ * @param e - The click event.
334
+ * @returns This router if it handled the click, otherwise `true`.
335
+ * @throws Whatever a guard or route callback throws.
336
+ */
213
337
  eventPushStateClick(e) {
214
338
  if (typeof window === 'undefined') {
215
339
  return true;
@@ -227,7 +351,8 @@ class Router {
227
351
  if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {
228
352
  return true;
229
353
  }
230
- const href = anchor.href || anchor.getAttribute('href') || '';
354
+ // Read the attribute, because on an SVG <a> the href property is an SVGAnimatedString, not a URL.
355
+ const href = anchor.getAttribute('href') ?? '';
231
356
  const baseUrl = typeof document !== 'undefined' && document.baseURI
232
357
  ? document.baseURI
233
358
  : window.location.href || `${window.location.origin}/`;
@@ -241,41 +366,74 @@ class Router {
241
366
  if (location.origin !== window.location.origin) {
242
367
  return true;
243
368
  }
369
+ // A link to a #fragment of the page already shown is native fragment navigation: let the browser scroll to it.
370
+ // Its popstate then only updates `url`, matching the fragment-only back/forward rule.
371
+ if (location.href.includes('#') && location.pathname === window.location.pathname && location.search === window.location.search) {
372
+ return true;
373
+ }
244
374
  const candidateRoute = this.matchRoute(location.pathname);
245
375
  const selected = candidateRoute ? this.routes[candidateRoute] : undefined;
246
- if (!selected || !this.isRunnableRoute(selected)) {
376
+ if (!selected || !this.isRunnableRoute(selected) || builtInDefaultRoutes.has(selected)) {
247
377
  return true;
248
378
  }
249
- e.preventDefault();
250
- this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false);
251
- return this;
379
+ // Guards are synchronous, so try the navigation first and cancel the browser's default only if Router took it.
380
+ // A rejected (or throwing) guard therefore falls back to a normal page load. If the route committed and then
381
+ // threw, the token has moved, so the default is still cancelled and the browser doesn't navigate twice.
382
+ const token = this.navigationToken;
383
+ let handled = false;
384
+ try {
385
+ handled = this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false) !== false;
386
+ }
387
+ finally {
388
+ if (handled || this.navigationToken !== token) {
389
+ e.preventDefault();
390
+ }
391
+ }
392
+ return handled ? this : true;
252
393
  }
253
- /** Restore browser history state without adding a new history entry. */
394
+ /**
395
+ * Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.
396
+ * A hash-only change just updates `url` and `locationData.url`.
397
+ * @returns This router.
398
+ */
254
399
  eventPopState() {
255
400
  if (typeof window === 'undefined') {
256
401
  return this;
257
402
  }
258
- this.navigate(this.getCurrentUrl(), {}, true);
403
+ const currentUrl = this.getCurrentUrl();
404
+ // Moving between #fragments on the same page (footnotes, skip links) keeps the current view and focus.
405
+ if (this.route !== null && withoutHash(currentUrl) === withoutHash(this.url)) {
406
+ this.url = currentUrl;
407
+ this.locationData = { ...this.locationData, url: currentUrl };
408
+ return this;
409
+ }
410
+ this.navigate(currentUrl, {}, true);
259
411
  return this;
260
412
  }
261
- /** Decode URL query parameters, keeping the last value for duplicate keys. */
413
+ /**
414
+ * Decode a query string into a plain object. For repeated keys the last value wins.
415
+ * @param queryString - Query string, with or without the leading `?`.
416
+ */
262
417
  parseQueryString(queryString) {
263
418
  return Object.fromEntries(new URLSearchParams(queryString));
264
419
  }
265
- /** Build decoded path, query, and mediator data for the selected route. */
420
+ /**
421
+ * Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.
422
+ * @param mediatorData - Value to expose as `locationData.data.mediator`.
423
+ */
266
424
  setLocationData(mediatorData) {
267
- const url = this.url || '/';
268
- let parsedUrl;
269
- try {
270
- const value = this.isBrowserRuntime() ? `${window.location.origin}${url}` : url;
271
- parsedUrl = new URL(value, this.getOrigin());
272
- }
273
- catch (_error) {
425
+ const parsedUrl = this.parseStoredUrl(this.url || '/');
426
+ if (!parsedUrl) {
274
427
  return;
275
428
  }
276
429
  this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);
277
430
  }
278
- /** Update page context after a route renders. DOM effects are browser-only. */
431
+ /**
432
+ * Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's
433
+ * `focus` selector (default `main h1`), adding `tabindex="-1"` if needed. `focus: false` skips focus.
434
+ * @param route - The route that just ran.
435
+ * @returns This router.
436
+ */
279
437
  applyPageContext(route) {
280
438
  if (typeof route === 'function') {
281
439
  this.pageTitle = null;
@@ -288,7 +446,7 @@ class Router {
288
446
  if (this.pageTitle) {
289
447
  document.title = this.pageTitle;
290
448
  }
291
- if (route.focus !== false) {
449
+ if (route.focus !== false && !this.suppressFocus) {
292
450
  const target = document.querySelector(route.focus || 'main h1');
293
451
  if (target) {
294
452
  if (!target.hasAttribute('tabindex')) {
@@ -299,25 +457,39 @@ class Router {
299
457
  }
300
458
  return this;
301
459
  }
302
- /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
460
+ /**
461
+ * Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,
462
+ * run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).
463
+ * If route code starts another navigation part-way through, that one wins and this call stops early.
464
+ * @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).
465
+ * @param mediatorData - Value exposed to the route as `location.data.mediator`.
466
+ * @param isPopState - `true` to skip adding a history entry.
467
+ * @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,
468
+ * guard refused, or nothing to run). A rejected navigation changes nothing.
469
+ * @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.
470
+ */
303
471
  navigate(url, mediatorData, isPopState = false) {
472
+ const skipFocus = this.initialDispatch;
473
+ this.initialDispatch = false;
304
474
  if (url !== undefined && typeof url !== 'string') {
305
475
  return false;
306
476
  }
307
- let candidateInput = url !== undefined ? url : this.url;
477
+ const candidateInput = url !== undefined ? url : this.url;
478
+ let parsedUrl;
308
479
  if (!candidateInput) {
309
- candidateInput = this.getCurrentUrl();
480
+ parsedUrl = this.parseNavigationUrl(this.getCurrentUrl());
481
+ }
482
+ else {
483
+ parsedUrl = url === undefined ? this.parseStoredUrl(candidateInput) : this.parseNavigationUrl(candidateInput);
310
484
  }
311
- const parsedUrl = this.parseNavigationUrl(candidateInput);
312
485
  if (!parsedUrl) {
313
486
  return false;
314
487
  }
315
488
  if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {
316
489
  return false;
317
490
  }
318
- const candidateUrl = this.isBrowserRuntime()
319
- ? `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`
320
- : candidateInput;
491
+ // Store only path, query, and hash, even when a server passed an absolute URL.
492
+ const candidateUrl = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
321
493
  const candidateRoute = this.matchRoute(parsedUrl.pathname);
322
494
  if (!candidateRoute) {
323
495
  return false;
@@ -328,34 +500,54 @@ class Router {
328
500
  !this.isRunnableRoute(selected)) {
329
501
  return false;
330
502
  }
503
+ const token = ++this.navigationToken;
331
504
  this.url = candidateUrl;
332
505
  this.route = candidateRoute;
333
506
  this.locationData = candidateLocationData;
334
- if (selected) {
335
- if (this.activeDestroy) {
336
- const { receiver, callback } = this.activeDestroy;
337
- callback.call(receiver, this.scope, this.locationData);
338
- this.activeDestroy = undefined;
339
- }
340
- if (typeof selected === 'function') {
341
- selected(this.scope, this.locationData);
507
+ // Clear the stored teardown before calling it, so a navigation started inside destroy() can't call it again,
508
+ // and a destroy() that throws is not retried: its error propagates with the destination committed but not run.
509
+ const previous = this.activeDestroy;
510
+ this.activeDestroy = undefined;
511
+ if (previous) {
512
+ previous.callback.call(previous.receiver, this.scope, candidateLocationData);
513
+ if (token !== this.navigationToken) {
514
+ return this;
342
515
  }
343
- else if (typeof selected.view === 'function') {
344
- selected.view(this.scope, this.locationData);
345
- }
346
- else {
347
- selected.view.initialize(this.scope, this.locationData);
348
- }
349
- const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'
350
- ? selected.view
351
- : undefined;
352
- this.activeDestroy = selectedView?.destroy
353
- ? { receiver: selectedView, callback: selectedView.destroy }
354
- : undefined;
516
+ }
517
+ const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'
518
+ ? selected.view
519
+ : undefined;
520
+ // Store the teardown before initialize() runs, so it still runs later if initialize() throws or navigates away.
521
+ this.activeDestroy = selectedView?.destroy
522
+ ? { receiver: selectedView, callback: selectedView.destroy }
523
+ : undefined;
524
+ if (typeof selected === 'function') {
525
+ selected(this.scope, candidateLocationData);
526
+ }
527
+ else if (typeof selected.view === 'function') {
528
+ selected.view(this.scope, candidateLocationData);
529
+ }
530
+ else {
531
+ selectedView.initialize(this.scope, candidateLocationData);
532
+ }
533
+ // If initialize() started another navigation, that one has already applied its own title, focus, and history.
534
+ if (token !== this.navigationToken) {
535
+ return this;
536
+ }
537
+ this.suppressFocus = skipFocus;
538
+ try {
355
539
  this.applyPageContext(selected);
356
- this.previousRoute = this.route;
357
540
  }
358
- if (!isPopState && this.isBrowserRuntime()) {
541
+ finally {
542
+ this.suppressFocus = false;
543
+ }
544
+ if (token !== this.navigationToken) {
545
+ return this;
546
+ }
547
+ this.previousRoute = this.route;
548
+ // Re-running the current URL (no `url` given, e.g. a mediator event without a destination) must not add a
549
+ // duplicate history entry for the address already shown.
550
+ if (!isPopState && this.isBrowserRuntime() && !(url === undefined && parsedUrl.href === window.location.href)) {
359
551
  window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);
360
552
  }
361
553
  return this;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/** Navigation payload, including caller-defined metadata. */\ninterface NavigationData {url?: string; [key: string]: unknown}\n/** URL-derived values supplied to route handlers. */\ninterface LocationData {url: string; data: {url: string[]; mediator: NavigationData | undefined; query: Record<string, string>}}\ntype RouteHandler = (scope: Element | null, location: LocationData) => unknown;\ninterface RouteViewLifecycle {initialize?: RouteHandler; destroy?: RouteHandler}\n/** An object route can own a function or a view lifecycle. */\ninterface RouteObject {title?: string; focus?: string | false; secure?: RouteHandler; view?: RouteHandler | RouteViewLifecycle}\ntype Route = RouteHandler | RouteObject;\ninterface ActiveDestroy {receiver: RouteViewLifecycle; callback: RouteHandler}\n/** Minimal standards-based event target contract required by the router. */\ninterface NavigationMediator {\n addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n/** History API router that also dispatches request URLs in non-browser runtimes. */\nclass Router {\n routes: Record<string, Route>;\n url: string;\n route: string | null;\n scope: Element | null;\n previousRoute: string | null;\n pageTitle: string | null;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: EventListener;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n private currentMediator: NavigationMediator | false = false;\n private boundMediator: NavigationMediator | false = false;\n private currentNavigationRoot: Document | Element | null = null;\n private boundNavigationRoot: Document | Element | null = null;\n private activeDestroy: ActiveDestroy | undefined;\n\n /** Create an instance with its own state and listener references. */\n constructor() {\n this.routes = {\n defaultRoute: {\n view: {\n initialize: () => true,\n destroy: () => true\n }\n }\n };\n this.url = '';\n this.route = null;\n this.scope = null;\n this.previousRoute = null;\n this.pageTitle = null;\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (event: Event) => {\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail === undefined || detail === null) {\n this.navigate(undefined, {}, false);\n return;\n }\n if (typeof detail !== 'object' || Array.isArray(detail)) {return;}\n const data = detail as NavigationData;\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n }\n\n get mediator(): NavigationMediator | false {return this.currentMediator;}\n\n /** Move an active router:navigate subscription when the mediator changes. */\n set mediator(value: NavigationMediator | false) {\n if (value === this.currentMediator) {return;}\n if (this.listenersInitialized && this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.currentMediator = value;\n if (this.listenersInitialized && value) {\n value.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = value;\n }\n }\n\n get navigationRoot(): Document | Element | null {return this.currentNavigationRoot;}\n\n /** Scope progressive link interception and move an active click listener when the root changes. */\n set navigationRoot(value: Document | Element | null) {\n if (value === this.currentNavigationRoot) {return;}\n if (this.listenersInitialized && this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.currentNavigationRoot = value;\n const nextRoot = value || document;\n nextRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = nextRoot;\n return;\n }\n this.currentNavigationRoot = value;\n }\n\n /** True when browser navigation APIs are available. */\n private isBrowserRuntime() {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n }\n\n /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */\n private getOrigin() {\n return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';\n }\n\n /** Read the authoritative browser URL, or retain the supplied server URL. */\n private getCurrentUrl() {\n if (!this.isBrowserRuntime()) {return this.url || '/';}\n return window.location.pathname + window.location.search + (window.location.hash || '');\n }\n\n /** Parse a navigation candidate without changing Router state. */\n private parseNavigationUrl(url: string) {\n try {\n const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();\n return new URL(url || '/', base);\n } catch (_error) {\n return null;\n }\n }\n\n /** Normalize browser navigation to a same-origin path, query, and hash. */\n private normalizeBrowserUrl(url: string) {\n if (!this.isBrowserRuntime()) {return url;}\n const parsedUrl = this.parseNavigationUrl(url);\n if (!parsedUrl || parsedUrl.origin !== window.location.origin) {return null;}\n return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n }\n\n /** Return whether a selected route has runnable view behavior. */\n private isRunnableRoute(route: Route): boolean {\n return typeof route === 'function' || typeof route.view === 'function' ||\n Boolean(route.view && typeof route.view.initialize === 'function');\n }\n /** Return the longest matching own route-table prefix, falling back to defaultRoute when configured. */\n private matchRoute(pathname: string): string | null {\n let candidateRoute: string | null = null;\n for (const route of Object.keys(this.routes)) {\n if (route === 'defaultRoute' || (pathname !== route && !pathname.startsWith(`${route}/`))) {continue;}\n if (!candidateRoute || route.length > candidateRoute.length) {candidateRoute = route;}\n }\n if (!candidateRoute && Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {\n return 'defaultRoute';\n }\n return candidateRoute;\n }\n\n\n /** Build decoded path, query, and mediator data for a navigation candidate. */\n private buildLocationData(url: string, route: string | null, mediatorData: NavigationData | undefined, parsedUrl: URL): LocationData {\n const matchedRoute = route && route !== 'defaultRoute' ? route : '';\n return {\n url,\n data: {\n url: parsedUrl.pathname\n .slice(matchedRoute.length)\n .split('/')\n .filter(Boolean)\n .map((fragment) => {\n try {return decodeURIComponent(fragment);} catch (_error) {return fragment;}\n }),\n mediator: mediatorData,\n query: this.parseQueryString(parsedUrl.search)\n }\n };\n }\n\n /**\n * Start this instance and return it for lifecycle chaining.\n * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.\n */\n initialize(url?: string) {\n this.addListeners();\n this.navigate(url ?? this.getCurrentUrl(), {}, true);\n return this;\n }\n\n /** Register browser and optional mediator handlers once. */\n addListeners() {\n if (this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n const navigationRoot = this.currentNavigationRoot || document;\n navigationRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = navigationRoot;\n window.addEventListener('popstate', this.boundPopState);\n }\n if (this.currentMediator) {\n this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = this.currentMediator;\n }\n this.listenersInitialized = true;\n return this;\n }\n\n /** Release owned state and listeners so the instance can leave the application lifecycle. */\n destroy() {\n this.removeListeners();\n return this;\n }\n\n /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */\n removeListeners() {\n if (!this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = null;\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.listenersInitialized = false;\n return this;\n }\n\n /** Intercept eligible same-origin push-state links while preserving normal browser actions. */\n eventPushStateClick(e: MouseEvent) {\n if (typeof window === 'undefined') {return true;}\n if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return true;\n }\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);\n const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;\n if (!anchor) {return true;}\n const target = anchor.getAttribute('target');\n if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {return true;}\n const href = (anchor as HTMLAnchorElement).href || anchor.getAttribute('href') || '';\n const baseUrl = typeof document !== 'undefined' && document.baseURI\n ? document.baseURI\n : window.location.href || `${window.location.origin}/`;\n let location: URL;\n try {location = new URL(href, baseUrl);} catch {return true;}\n if (location.origin !== window.location.origin) {return true;}\n const candidateRoute = this.matchRoute(location.pathname);\n const selected = candidateRoute ? this.routes[candidateRoute] : undefined;\n if (!selected || !this.isRunnableRoute(selected)) {return true;}\n e.preventDefault();\n this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false);\n return this;\n }\n\n /** Restore browser history state without adding a new history entry. */\n eventPopState() {\n if (typeof window === 'undefined') {return this;}\n this.navigate(this.getCurrentUrl(), {}, true);\n return this;\n }\n\n /** Decode URL query parameters, keeping the last value for duplicate keys. */\n parseQueryString(queryString: string) {\n return Object.fromEntries(new URLSearchParams(queryString));\n }\n\n /** Build decoded path, query, and mediator data for the selected route. */\n setLocationData(mediatorData?: NavigationData) {\n const url = this.url || '/';\n let parsedUrl: URL;\n try {\n const value = this.isBrowserRuntime() ? `${window.location.origin}${url}` : url;\n parsedUrl = new URL(value, this.getOrigin());\n } catch (_error) {\n return;\n }\n this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);\n }\n\n /** Update page context after a route renders. DOM effects are browser-only. */\n applyPageContext(route: Route) {\n if (typeof route === 'function') {\n this.pageTitle = null;\n return this;\n }\n this.pageTitle = typeof route.title === 'string' ? route.title : null;\n if (!this.isBrowserRuntime()) {return this;}\n if (this.pageTitle) {document.title = this.pageTitle;}\n if (route.focus !== false) {\n const target = document.querySelector<HTMLElement>(route.focus || 'main h1');\n if (target) {\n if (!target.hasAttribute('tabindex')) {target.setAttribute('tabindex', '-1');}\n target.focus();\n }\n }\n return this;\n }\n\n /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */\n navigate(url?: string, mediatorData?: NavigationData, isPopState = false) {\n if (url !== undefined && typeof url !== 'string') {return false;}\n\n let candidateInput = url !== undefined ? url : this.url;\n if (!candidateInput) {candidateInput = this.getCurrentUrl();}\n const parsedUrl = this.parseNavigationUrl(candidateInput);\n if (!parsedUrl) {return false;}\n if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {return false;}\n\n const candidateUrl = this.isBrowserRuntime()\n ? `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`\n : candidateInput;\n const candidateRoute = this.matchRoute(parsedUrl.pathname);\n if (!candidateRoute) {return false;}\n\n const candidateLocationData = this.buildLocationData(candidateUrl, candidateRoute, mediatorData, parsedUrl);\n const selected = this.routes[candidateRoute];\n if (!selected || (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, candidateLocationData) !== true) ||\n !this.isRunnableRoute(selected)) {\n return false;\n }\n\n this.url = candidateUrl;\n this.route = candidateRoute;\n this.locationData = candidateLocationData;\n if (selected) {\n if (this.activeDestroy) {\n const {receiver, callback} = this.activeDestroy;\n callback.call(receiver, this.scope, this.locationData);\n this.activeDestroy = undefined;\n }\n if (typeof selected === 'function') {\n selected(this.scope, this.locationData);\n } else if (typeof selected.view === 'function') {\n selected.view(this.scope, this.locationData);\n } else {\n selected.view!.initialize!(this.scope, this.locationData);\n }\n const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'\n ? selected.view\n : undefined;\n this.activeDestroy = selectedView?.destroy\n ? {receiver: selectedView, callback: selectedView.destroy}\n : undefined;\n this.applyPageContext(selected);\n this.previousRoute = this.route;\n }\n if (!isPopState && this.isBrowserRuntime()) {\n window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);\n }\n return this;\n }\n}\n\n/** Public route configuration and navigation payload types. */\nnamespace Router {\n export type Handler = RouteHandler;\n export type Route = RouteHandler | RouteObject;\n export type Location = LocationData;\n export type Navigation = NavigationData;\n}\nexport = Router;\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/** Extra data passed with a navigation. `url`, if present, is the destination; any other fields are yours. */\ninterface NavigationData {url?: string; [key: string]: unknown}\n/**\n * What a route callback receives about the destination.\n * `url` is the path, query, and hash; `data.url` holds the decoded path segments after the matched route key;\n * `data.query` holds decoded query values; `data.mediator` is the navigation data, if any.\n */\ninterface LocationData {url: string; data: {url: string[]; mediator: NavigationData | undefined; query: Record<string, string>}}\n/** A route callback. It receives `router.scope` and the destination location. */\ntype RouteHandler = (scope: Element | null, location: LocationData) => unknown;\n/** A view with setup and teardown. `destroy` receives the location being navigated to. */\ninterface RouteViewLifecycle {initialize?: RouteHandler; destroy?: RouteHandler}\n/**\n * A route with optional page context and a guard.\n * `secure` must return exactly `true` to allow navigation; `focus: false` leaves focus where it is.\n */\ninterface RouteObject {title?: string; focus?: string | false; secure?: RouteHandler; view?: RouteHandler | RouteViewLifecycle}\ntype Route = RouteHandler | RouteObject;\n/** The teardown captured from the view that actually initialized, so later route-table edits can't change it. */\ninterface ActiveDestroy {receiver: RouteViewLifecycle; callback: RouteHandler}\n/** Any EventTarget-like object; Router only adds and removes a `router:navigate` listener on it. */\ninterface NavigationMediator {\n addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n/**\n * Route keys prepared for fast matching. `prefixes` maps a normalized path to the route key that owns it;\n * `prefixFor` maps each key to its normalized path. Rebuilt only when the list of own keys changes.\n */\ninterface CompiledRoutes {keys: string[]; prefixes: Map<string, string>; prefixFor: Map<string, string>}\n\n/** The do-nothing `defaultRoute` objects each constructor installs, so link clicks can tell them apart from app routes. */\nconst builtInDefaultRoutes = new WeakSet<object>();\n/** A dummy origin that lets route keys and server URLs go through the standard URL parser. */\nconst routeKeyBase = 'http://white-label.invalid';\n\n/** Encode a route key the way the URL parser encodes paths, so `'/café'` matches `/caf%C3%A9`. */\nfunction normalizeRouteKey(key: string) {\n // A key that doesn't start with '/' or contains '?' or '#' can never equal a pathname, so leave it as is (it won't match).\n if (!key.startsWith('/') || /[?#]/.test(key)) {return key;}\n return new URL(`${routeKeyBase}${key}`).pathname;\n}\n\n/** Strip the `#fragment`, leaving the path and query. */\nfunction withoutHash(url: string) {\n const index = url.indexOf('#');\n return index === -1 ? url : url.slice(0, index);\n}\n\n/**\n * Maps URLs to route callbacks. In a browser it enhances `data-pushstate` links and back/forward with the History API;\n * on a server it dispatches the request URL you pass, with no DOM required.\n */\nclass Router {\n routes: Record<string, Route>;\n url: string;\n route: string | null;\n scope: Element | null;\n previousRoute: string | null;\n pageTitle: string | null;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: EventListener;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n private currentMediator: NavigationMediator | false = false;\n private boundMediator: NavigationMediator | false = false;\n private currentNavigationRoot: Document | Element | null = null;\n private boundNavigationRoot: Document | Element | null = null;\n private activeDestroy: ActiveDestroy | undefined;\n private compiledRoutes: CompiledRoutes | undefined;\n // The routes object indexRoutes() indexed. While `routes` is still that object, matching trusts the index as is.\n private indexedRoutes: Record<string, Route> | undefined;\n // Bumped by each navigation that passes its guard. If it changes while a navigation is calling out to app code,\n // a newer navigation has started, and the older one stops before touching title, focus, or history.\n private navigationToken = 0;\n // True only for the first navigate() call made by initialize(), so that dispatch leaves focus alone.\n private initialDispatch = false;\n // Tells applyPageContext() to skip focus for the current call.\n private suppressFocus = false;\n\n /** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */\n constructor() {\n const defaultRoute: RouteObject = {\n view: {\n initialize: () => true,\n destroy: () => true\n }\n };\n builtInDefaultRoutes.add(defaultRoute);\n this.routes = {defaultRoute};\n this.url = '';\n this.route = null;\n this.scope = null;\n this.previousRoute = null;\n this.pageTitle = null;\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (event: Event) => {\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail === undefined || detail === null) {\n this.navigate(undefined, {}, false);\n return;\n }\n if (typeof detail !== 'object' || Array.isArray(detail)) {return;}\n const data = detail as NavigationData;\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n }\n\n /** Event target that can send `router:navigate` events, or `false` for none. */\n get mediator(): NavigationMediator | false {return this.currentMediator;}\n\n /** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */\n set mediator(value: NavigationMediator | false) {\n if (value === this.currentMediator) {return;}\n if (this.listenersInitialized && this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.currentMediator = value;\n if (this.listenersInitialized && value) {\n value.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = value;\n }\n }\n\n /** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */\n get navigationRoot(): Document | Element | null {return this.currentNavigationRoot;}\n\n /** Set the click root. If listeners are active, the click listener moves to the new root. */\n set navigationRoot(value: Document | Element | null) {\n if (value === this.currentNavigationRoot) {return;}\n if (this.listenersInitialized && this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.currentNavigationRoot = value;\n const nextRoot = value || document;\n nextRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = nextRoot;\n return;\n }\n this.currentNavigationRoot = value;\n }\n\n /** True when `window` and `document` exist, i.e. browser-only effects should run. */\n private isBrowserRuntime() {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n }\n\n /** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */\n private getOrigin() {\n return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';\n }\n\n /** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */\n private getCurrentUrl() {\n if (!this.isBrowserRuntime()) {return this.url || '/';}\n return window.location.pathname + window.location.search + (window.location.hash || '');\n }\n\n /** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */\n private parseNavigationUrl(url: string) {\n try {\n const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();\n return new URL(url || '/', base);\n } catch (_error) {\n return null;\n }\n }\n\n /** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */\n private normalizeBrowserUrl(url: string) {\n if (!this.isBrowserRuntime()) {return url;}\n const parsedUrl = this.parseNavigationUrl(url);\n if (!parsedUrl || parsedUrl.origin !== window.location.origin) {return null;}\n return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n }\n\n /** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */\n private isRunnableRoute(route: Route): boolean {\n return typeof route === 'function' || typeof route.view === 'function' ||\n Boolean(route.view && typeof route.view.initialize === 'function');\n }\n\n /**\n * Index the current route keys and match against that index without re-reading the keys on every navigation.\n * Without it, each match lists the keys of `routes` to notice in-place edits, which costs time in proportion to\n * the number of routes. Use it for large route tables that stop changing after setup. Assigning a different object to\n * `routes` drops the index; after adding or removing keys on the same object, call `indexRoutes()` again. Until then,\n * an added key is not matched, and a removed key still claims its paths, so `navigate()` returns `false` for them\n * as it does for a key set to `undefined`.\n * Replacing the value of an existing key needs no re-index, because the route is looked up when it runs.\n * @returns This router.\n */\n indexRoutes() {\n this.indexedRoutes = undefined;\n this.compileRoutes();\n this.indexedRoutes = this.routes;\n return this;\n }\n\n /** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */\n private compileRoutes(): CompiledRoutes {\n const cached = this.compiledRoutes;\n if (cached && this.indexedRoutes === this.routes) {return cached;}\n this.indexedRoutes = undefined;\n const keys = Object.keys(this.routes);\n if (cached && cached.keys.length === keys.length && cached.keys.every((key, index) => key === keys[index])) {\n return cached;\n }\n const prefixes = new Map<string, string>();\n const prefixFor = new Map<string, string>();\n for (const key of keys) {\n if (key === 'defaultRoute') {continue;}\n const prefix = normalizeRouteKey(key);\n prefixFor.set(key, prefix);\n const existing = prefixes.get(prefix);\n // When two keys normalize to the same path, prefer the one already in normalized form, then the alphabetically\n // lowest, so the winner never depends on insertion order.\n if (existing === undefined || (existing !== prefix && (key === prefix || key < existing))) {prefixes.set(prefix, key);}\n }\n this.compiledRoutes = {keys, prefixes, prefixFor};\n return this.compiledRoutes;\n }\n\n /** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */\n private matchRoute(pathname: string): string | null {\n const {prefixes} = this.compileRoutes();\n // Try the full path, then drop one '/segment' at a time; the first hit is the longest match.\n // Cutting only at '/' means '/products' matches '/products/42' but not '/products-old'.\n let candidateRoute = prefixes.get(pathname);\n let index = pathname.lastIndexOf('/');\n while (candidateRoute === undefined && index >= 0) {\n candidateRoute = prefixes.get(pathname.slice(0, index));\n index = index > 0 ? pathname.lastIndexOf('/', index - 1) : -1;\n }\n if (candidateRoute !== undefined) {return candidateRoute;}\n if (Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {\n return 'defaultRoute';\n }\n return null;\n }\n\n /** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */\n private routePrefixLength(route: string | null) {\n if (!route || route === 'defaultRoute') {return 0;}\n return (this.compileRoutes().prefixFor.get(route) ?? normalizeRouteKey(route)).length;\n }\n\n /** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */\n private parseStoredUrl(url: string) {\n if (!url.startsWith('/')) {return this.parseNavigationUrl(url);}\n try {\n return new URL(`${this.getOrigin()}${url}`);\n } catch (_error) {\n return null;\n }\n }\n\n /** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */\n private buildLocationData(url: string, route: string | null, mediatorData: NavigationData | undefined, parsedUrl: URL): LocationData {\n return {\n url,\n data: {\n url: parsedUrl.pathname\n .slice(this.routePrefixLength(route))\n .split('/')\n .filter(Boolean)\n .map((fragment) => {\n try {return decodeURIComponent(fragment);} catch (_error) {return fragment;}\n }),\n mediator: mediatorData,\n query: this.parseQueryString(parsedUrl.search)\n }\n };\n }\n\n /**\n * Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.\n * @param url - Request URL on a server. In a browser, omit it to use `window.location`.\n * @returns This router.\n * @throws Whatever a guard or route callback throws.\n */\n initialize(url?: string) {\n this.addListeners();\n // The browser has already placed focus on the page it just loaded, so only later navigations move it.\n this.initialDispatch = true;\n try {\n this.navigate(url ?? this.getCurrentUrl(), {}, true);\n } finally {\n this.initialDispatch = false;\n }\n return this;\n }\n\n /**\n * Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.\n * @returns This router.\n */\n addListeners() {\n if (this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n const navigationRoot = this.currentNavigationRoot || document;\n navigationRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = navigationRoot;\n window.addEventListener('popstate', this.boundPopState);\n }\n if (this.currentMediator) {\n this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = this.currentMediator;\n }\n this.listenersInitialized = true;\n return this;\n }\n\n /**\n * Remove this router's listeners. Does not call the active route's `destroy()`.\n * @returns This router.\n */\n destroy() {\n this.removeListeners();\n return this;\n }\n\n /**\n * Remove the listeners `addListeners()` attached. Subclasses can override this to release more.\n * @returns This router.\n */\n removeListeners() {\n if (!this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = null;\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.listenersInitialized = false;\n return this;\n }\n\n /**\n * Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:\n * a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,\n * runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left\n * to the browser.\n * @param e - The click event.\n * @returns This router if it handled the click, otherwise `true`.\n * @throws Whatever a guard or route callback throws.\n */\n eventPushStateClick(e: MouseEvent) {\n if (typeof window === 'undefined') {return true;}\n if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return true;\n }\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);\n const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;\n if (!anchor) {return true;}\n const target = anchor.getAttribute('target');\n if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {return true;}\n // Read the attribute, because on an SVG <a> the href property is an SVGAnimatedString, not a URL.\n const href = anchor.getAttribute('href') ?? '';\n const baseUrl = typeof document !== 'undefined' && document.baseURI\n ? document.baseURI\n : window.location.href || `${window.location.origin}/`;\n let location: URL;\n try {location = new URL(href, baseUrl);} catch {return true;}\n if (location.origin !== window.location.origin) {return true;}\n // A link to a #fragment of the page already shown is native fragment navigation: let the browser scroll to it.\n // Its popstate then only updates `url`, matching the fragment-only back/forward rule.\n if (location.href.includes('#') && location.pathname === window.location.pathname && location.search === window.location.search) {\n return true;\n }\n const candidateRoute = this.matchRoute(location.pathname);\n const selected = candidateRoute ? this.routes[candidateRoute] : undefined;\n if (!selected || !this.isRunnableRoute(selected) || builtInDefaultRoutes.has(selected)) {return true;}\n // Guards are synchronous, so try the navigation first and cancel the browser's default only if Router took it.\n // A rejected (or throwing) guard therefore falls back to a normal page load. If the route committed and then\n // threw, the token has moved, so the default is still cancelled and the browser doesn't navigate twice.\n const token = this.navigationToken;\n let handled = false;\n try {\n handled = this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false) !== false;\n } finally {\n if (handled || this.navigationToken !== token) {e.preventDefault();}\n }\n return handled ? this : true;\n }\n\n /**\n * Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.\n * A hash-only change just updates `url` and `locationData.url`.\n * @returns This router.\n */\n eventPopState() {\n if (typeof window === 'undefined') {return this;}\n const currentUrl = this.getCurrentUrl();\n // Moving between #fragments on the same page (footnotes, skip links) keeps the current view and focus.\n if (this.route !== null && withoutHash(currentUrl) === withoutHash(this.url)) {\n this.url = currentUrl;\n this.locationData = {...this.locationData, url: currentUrl};\n return this;\n }\n this.navigate(currentUrl, {}, true);\n return this;\n }\n\n /**\n * Decode a query string into a plain object. For repeated keys the last value wins.\n * @param queryString - Query string, with or without the leading `?`.\n */\n parseQueryString(queryString: string) {\n return Object.fromEntries(new URLSearchParams(queryString));\n }\n\n /**\n * Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.\n * @param mediatorData - Value to expose as `locationData.data.mediator`.\n */\n setLocationData(mediatorData?: NavigationData) {\n const parsedUrl = this.parseStoredUrl(this.url || '/');\n if (!parsedUrl) {return;}\n this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);\n }\n\n /**\n * Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's\n * `focus` selector (default `main h1`), adding `tabindex=\"-1\"` if needed. `focus: false` skips focus.\n * @param route - The route that just ran.\n * @returns This router.\n */\n applyPageContext(route: Route) {\n if (typeof route === 'function') {\n this.pageTitle = null;\n return this;\n }\n this.pageTitle = typeof route.title === 'string' ? route.title : null;\n if (!this.isBrowserRuntime()) {return this;}\n if (this.pageTitle) {document.title = this.pageTitle;}\n if (route.focus !== false && !this.suppressFocus) {\n const target = document.querySelector<HTMLElement>(route.focus || 'main h1');\n if (target) {\n if (!target.hasAttribute('tabindex')) {target.setAttribute('tabindex', '-1');}\n target.focus();\n }\n }\n return this;\n }\n\n /**\n * Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,\n * run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).\n * If route code starts another navigation part-way through, that one wins and this call stops early.\n * @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).\n * @param mediatorData - Value exposed to the route as `location.data.mediator`.\n * @param isPopState - `true` to skip adding a history entry.\n * @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,\n * guard refused, or nothing to run). A rejected navigation changes nothing.\n * @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.\n */\n navigate(url?: string, mediatorData?: NavigationData, isPopState = false) {\n const skipFocus = this.initialDispatch;\n this.initialDispatch = false;\n if (url !== undefined && typeof url !== 'string') {return false;}\n\n const candidateInput = url !== undefined ? url : this.url;\n let parsedUrl: URL | null;\n if (!candidateInput) {\n parsedUrl = this.parseNavigationUrl(this.getCurrentUrl());\n } else {\n parsedUrl = url === undefined ? this.parseStoredUrl(candidateInput) : this.parseNavigationUrl(candidateInput);\n }\n if (!parsedUrl) {return false;}\n if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {return false;}\n\n // Store only path, query, and hash, even when a server passed an absolute URL.\n const candidateUrl = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n const candidateRoute = this.matchRoute(parsedUrl.pathname);\n if (!candidateRoute) {return false;}\n\n const candidateLocationData = this.buildLocationData(candidateUrl, candidateRoute, mediatorData, parsedUrl);\n const selected = this.routes[candidateRoute];\n if (!selected || (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, candidateLocationData) !== true) ||\n !this.isRunnableRoute(selected)) {\n return false;\n }\n\n const token = ++this.navigationToken;\n this.url = candidateUrl;\n this.route = candidateRoute;\n this.locationData = candidateLocationData;\n // Clear the stored teardown before calling it, so a navigation started inside destroy() can't call it again,\n // and a destroy() that throws is not retried: its error propagates with the destination committed but not run.\n const previous = this.activeDestroy;\n this.activeDestroy = undefined;\n if (previous) {\n previous.callback.call(previous.receiver, this.scope, candidateLocationData);\n if (token !== this.navigationToken) {return this;}\n }\n const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'\n ? selected.view\n : undefined;\n // Store the teardown before initialize() runs, so it still runs later if initialize() throws or navigates away.\n this.activeDestroy = selectedView?.destroy\n ? {receiver: selectedView, callback: selectedView.destroy}\n : undefined;\n if (typeof selected === 'function') {\n selected(this.scope, candidateLocationData);\n } else if (typeof selected.view === 'function') {\n selected.view(this.scope, candidateLocationData);\n } else {\n selectedView!.initialize!(this.scope, candidateLocationData);\n }\n // If initialize() started another navigation, that one has already applied its own title, focus, and history.\n if (token !== this.navigationToken) {return this;}\n this.suppressFocus = skipFocus;\n try {\n this.applyPageContext(selected);\n } finally {\n this.suppressFocus = false;\n }\n if (token !== this.navigationToken) {return this;}\n this.previousRoute = this.route;\n // Re-running the current URL (no `url` given, e.g. a mediator event without a destination) must not add a\n // duplicate history entry for the address already shown.\n if (!isPopState && this.isBrowserRuntime() && !(url === undefined && parsedUrl.href === window.location.href)) {\n window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);\n }\n return this;\n }\n}\n\n/** Types for route definitions, location data, and navigation data. */\nnamespace Router {\n export type Handler = RouteHandler;\n export type Route = RouteHandler | RouteObject;\n export type Location = LocationData;\n export type Navigation = NavigationData;\n}\nexport = Router;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "white-label-router",
3
- "version": "6.1.2",
3
+ "version": "6.3.0",
4
4
  "description": "Framework-independent TypeScript router for progressive enhancement in browser, server-rendered, and Node.js applications, with scoped links, guards, and accessible navigation.",
5
5
  "sideEffects": false,
6
6
  "main": "dist/index.js",
@@ -54,21 +54,25 @@
54
54
  "audit": "npm audit --audit-level=high",
55
55
  "typecheck": "tsc -p tsconfig.json --noEmit",
56
56
  "test:unit": "node --test test/*.test.js",
57
+ "test:browser": "node --test test/browser/*.test.js",
57
58
  "coverage": "node --run build && node --run test:types && c8 node --run test:unit",
59
+ "benchmark": "node benchmark/router.bench.js",
60
+ "bench:ci": "node benchmark/router.bench.js --ci",
58
61
  "prepare": "node --run build",
59
62
  "test:types": "tsc -p tsconfig.consumer.json",
60
- "lint": "eslint \"src/**/*.ts\" --max-warnings=0",
63
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.js\" \"benchmark/**/*.js\" --max-warnings=0",
61
64
  "lint:fix": "node --run lint -- --fix"
62
65
  },
63
66
  "devDependencies": {
64
- "@types/node": "24.13.3",
65
- "c8": "12.0.0",
66
- "typescript": "7.0.2",
67
- "@babel/core": "8.0.5",
68
- "@babel/eslint-parser": "8.0.5",
69
- "@babel/preset-typescript": "8.0.1",
67
+ "@babel/core": "8.0.7",
68
+ "@babel/eslint-parser": "8.0.7",
69
+ "@babel/preset-typescript": "8.0.7",
70
70
  "@eslint/js": "10.0.1",
71
- "eslint": "10.10.0"
71
+ "@types/node": "24.19.2",
72
+ "c8": "12.0.0",
73
+ "eslint": "10.13.0",
74
+ "playwright": "1.64.1",
75
+ "typescript": "7.0.2"
72
76
  },
73
77
  "types": "dist/index.d.ts",
74
78
  "c8": {
@@ -89,7 +93,9 @@
89
93
  "branches": 100,
90
94
  "statements": 100,
91
95
  "perFile": true,
92
- "exclude": ["**/*.d.ts"]
96
+ "exclude": [
97
+ "**/*.d.ts"
98
+ ]
93
99
  },
94
100
  "exports": {
95
101
  ".": {