white-label-router 6.1.2 → 6.2.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
 
@@ -145,7 +156,28 @@ router.routes = {
145
156
  };
146
157
  ```
147
158
 
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.
159
+ ### Guards
160
+
161
+ `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.
162
+
163
+ ### Lifecycle order
164
+
165
+ A successful navigation runs these steps in order:
166
+
167
+ 1. Match the route and run its guard. Rejection stops here with no state change.
168
+ 2. Commit `router.url`, `router.route`, and `router.locationData` for the destination.
169
+ 3. Call the previous route's `destroy(scope, nextLocation)`, if it had one.
170
+ 4. Run the new route: a function route, a `view` function, or `view.initialize(scope, location)`.
171
+ 5. Apply the route's title and focus (browser only; see [Title and focus](#title-and-focus)).
172
+ 6. Set `router.previousRoute`, then push a history entry (browser only; skipped for `initialize()` and back/forward).
173
+
174
+ 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.
175
+
176
+ 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.
177
+
178
+ ### Navigating from inside a route
179
+
180
+ `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
181
 
150
182
  ## Matching
151
183
 
@@ -163,6 +195,10 @@ router.routes = {
163
195
 
164
196
  `/products/sale` selects `saleRoute` even though `/products` was defined first. When no prefix matches, `defaultRoute` runs if configured.
165
197
 
198
+ 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.
199
+
200
+ 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.
201
+
166
202
  ## Location contract
167
203
 
168
204
  Routes receive the configured `scope` and a location object:
@@ -178,7 +214,12 @@ Routes receive the configured `scope` and a location object:
178
214
  }
179
215
  ```
180
216
 
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.
217
+ - `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.
218
+ - `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.
219
+ - `data.query` contains query values decoded with `URLSearchParams`; for duplicate keys the last value wins.
220
+ - `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.
221
+
222
+ Applications must still validate these untrusted values.
182
223
 
183
224
  ## Programmatic navigation
184
225
 
@@ -192,7 +233,7 @@ In browsers, successful navigation updates history unless the call represents `p
192
233
 
193
234
  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
235
 
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.
236
+ 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
237
 
197
238
  ## Mediator integration
198
239
 
@@ -214,7 +255,7 @@ mediator.dispatchEvent(new CustomEvent('router:navigate', {
214
255
  }));
215
256
  ```
216
257
 
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.
258
+ 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
259
 
219
260
  Router does not import or require `white-label-mediator`; the integration is structural so another compatible EventTarget can be used instead.
220
261
 
@@ -259,12 +300,14 @@ router.routes = {
259
300
  };
260
301
  ```
261
302
 
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.
303
+ After a route renders, Router sets `document.title` from `title` and focuses the first element matching `focus` (default `main h1`), 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
304
 
264
305
  ## Browser history behavior
265
306
 
266
307
  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
308
 
309
+ 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.
310
+
268
311
  ## Public API
269
312
 
270
313
  | Member | Behavior | Returns |
@@ -277,12 +320,14 @@ Initialization dispatches the current URL without adding a duplicate history ent
277
320
  | `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
321
  | `addListeners()` | Attach browser and optional mediator listeners once. | The same `Router` instance. |
279
322
  | `removeListeners()` | Release listeners owned by this router. | The same `Router` instance. |
280
- | `destroy()` | Release routing listeners. | The same `Router` instance after cleanup. |
323
+ | `destroy()` | Release routing listeners. Does not call the active route's `destroy()`. | The same `Router` instance after cleanup. |
281
324
  | `parseQueryString(query)` | Decode a query string using `URLSearchParams`. | A plain object containing the last value for each query key. |
282
325
  | `setLocationData(data?)` | Rebuild the current route location payload. | `undefined`; updates `locationData` in place. |
283
326
  | `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. |
327
+ | `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. |
328
+ | `eventPopState()` | Dispatch the current browser history location; a hash-only change just updates `url`. | The same `Router` instance. |
329
+
330
+ Router records navigation state on public properties that applications should treat as read-only: `url`, `route`, `previousRoute`, `locationData`, and `pageTitle`.
286
331
 
287
332
  ## TypeScript
288
333
 
@@ -304,17 +349,19 @@ router.initialize();
304
349
 
305
350
  ```sh
306
351
  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
352
+ npm run lint && npm run coverage && npm run audit
313
353
  ```
314
354
 
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.
355
+ `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.
356
+
357
+ After `npm run build`:
358
+
359
+ - `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.
360
+ - `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.
361
+
362
+ 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
363
 
317
- Edit `src/*.ts` and regenerate `dist`; do not edit generated files directly.
364
+ Edit `src/index.ts`; `dist` is build output and is not edited or committed. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
318
365
 
319
366
  ## Design boundary
320
367
 
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,113 @@ 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 navigationToken;
65
+ private initialDispatch;
66
+ private suppressFocus;
67
+ /** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */
52
68
  constructor();
69
+ /** Event target that can send `router:navigate` events, or `false` for none. */
53
70
  get mediator(): NavigationMediator | false;
54
- /** Move an active router:navigate subscription when the mediator changes. */
71
+ /** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */
55
72
  set mediator(value: NavigationMediator | false);
73
+ /** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */
56
74
  get navigationRoot(): Document | Element | null;
57
- /** Scope progressive link interception and move an active click listener when the root changes. */
75
+ /** Set the click root. If listeners are active, the click listener moves to the new root. */
58
76
  set navigationRoot(value: Document | Element | null);
59
- /** True when browser navigation APIs are available. */
77
+ /** True when `window` and `document` exist, i.e. browser-only effects should run. */
60
78
  private isBrowserRuntime;
61
- /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
79
+ /** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */
62
80
  private getOrigin;
63
- /** Read the authoritative browser URL, or retain the supplied server URL. */
81
+ /** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */
64
82
  private getCurrentUrl;
65
- /** Parse a navigation candidate without changing Router state. */
83
+ /** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */
66
84
  private parseNavigationUrl;
67
- /** Normalize browser navigation to a same-origin path, query, and hash. */
85
+ /** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */
68
86
  private normalizeBrowserUrl;
69
- /** Return whether a selected route has runnable view behavior. */
87
+ /** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */
70
88
  private isRunnableRoute;
71
- /** Return the longest matching own route-table prefix, falling back to defaultRoute when configured. */
89
+ /** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */
90
+ private compileRoutes;
91
+ /** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */
72
92
  private matchRoute;
73
- /** Build decoded path, query, and mediator data for a navigation candidate. */
93
+ /** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */
94
+ private routePrefixLength;
95
+ /** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */
96
+ private parseStoredUrl;
97
+ /** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */
74
98
  private buildLocationData;
75
99
  /**
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.
100
+ * Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.
101
+ * @param url - Request URL on a server. In a browser, omit it to use `window.location`.
102
+ * @returns This router.
103
+ * @throws Whatever a guard or route callback throws.
78
104
  */
79
105
  initialize(url?: string): this;
80
- /** Register browser and optional mediator handlers once. */
106
+ /**
107
+ * Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.
108
+ * @returns This router.
109
+ */
81
110
  addListeners(): this;
82
- /** Release owned state and listeners so the instance can leave the application lifecycle. */
111
+ /**
112
+ * Remove this router's listeners. Does not call the active route's `destroy()`.
113
+ * @returns This router.
114
+ */
83
115
  destroy(): this;
84
- /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
116
+ /**
117
+ * Remove the listeners `addListeners()` attached. Subclasses can override this to release more.
118
+ * @returns This router.
119
+ */
85
120
  removeListeners(): this;
86
- /** Intercept eligible same-origin push-state links while preserving normal browser actions. */
121
+ /**
122
+ * Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:
123
+ * a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,
124
+ * runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left
125
+ * to the browser.
126
+ * @param e - The click event.
127
+ * @returns This router if it handled the click, otherwise `true`.
128
+ * @throws Whatever a guard or route callback throws.
129
+ */
87
130
  eventPushStateClick(e: MouseEvent): true | this;
88
- /** Restore browser history state without adding a new history entry. */
131
+ /**
132
+ * Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.
133
+ * A hash-only change just updates `url` and `locationData.url`.
134
+ * @returns This router.
135
+ */
89
136
  eventPopState(): this;
90
- /** Decode URL query parameters, keeping the last value for duplicate keys. */
137
+ /**
138
+ * Decode a query string into a plain object. For repeated keys the last value wins.
139
+ * @param queryString - Query string, with or without the leading `?`.
140
+ */
91
141
  parseQueryString(queryString: string): {
92
142
  [k: string]: string;
93
143
  };
94
- /** Build decoded path, query, and mediator data for the selected route. */
144
+ /**
145
+ * Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.
146
+ * @param mediatorData - Value to expose as `locationData.data.mediator`.
147
+ */
95
148
  setLocationData(mediatorData?: NavigationData): void;
96
- /** Update page context after a route renders. DOM effects are browser-only. */
149
+ /**
150
+ * Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's
151
+ * `focus` selector (default `main h1`), adding `tabindex="-1"` if needed. `focus: false` skips focus.
152
+ * @param route - The route that just ran.
153
+ * @returns This router.
154
+ */
97
155
  applyPageContext(route: Route): this;
98
- /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
156
+ /**
157
+ * Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,
158
+ * run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).
159
+ * If route code starts another navigation part-way through, that one wins and this call stops early.
160
+ * @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).
161
+ * @param mediatorData - Value exposed to the route as `location.data.mediator`.
162
+ * @param isPopState - `true` to skip adding a history entry.
163
+ * @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,
164
+ * guard refused, or nothing to run). A rejected navigation changes nothing.
165
+ * @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.
166
+ */
99
167
  navigate(url?: string, mediatorData?: NavigationData, isPopState?: boolean): false | this;
100
168
  }
101
- /** Public route configuration and navigation payload types. */
169
+ /** Types for route definitions, location data, and navigation data. */
102
170
  declare namespace Router {
103
171
  type Handler = RouteHandler;
104
172
  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,24 @@ 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
+ // Bumped by each navigation that passes its guard. If it changes while a navigation is calling out to app code,
42
+ // a newer navigation has started, and the older one stops before touching title, focus, or history.
43
+ navigationToken = 0;
44
+ // True only for the first navigate() call made by initialize(), so that dispatch leaves focus alone.
45
+ initialDispatch = false;
46
+ // Tells applyPageContext() to skip focus for the current call.
47
+ suppressFocus = false;
48
+ /** Create a router with a placeholder `defaultRoute` and no listeners attached yet. */
21
49
  constructor() {
22
- this.routes = {
23
- defaultRoute: {
24
- view: {
25
- initialize: () => true,
26
- destroy: () => true
27
- }
50
+ const defaultRoute = {
51
+ view: {
52
+ initialize: () => true,
53
+ destroy: () => true
28
54
  }
29
55
  };
56
+ builtInDefaultRoutes.add(defaultRoute);
57
+ this.routes = { defaultRoute };
30
58
  this.url = '';
31
59
  this.route = null;
32
60
  this.scope = null;
@@ -48,8 +76,9 @@ class Router {
48
76
  };
49
77
  this.listenersInitialized = false;
50
78
  }
79
+ /** Event target that can send `router:navigate` events, or `false` for none. */
51
80
  get mediator() { return this.currentMediator; }
52
- /** Move an active router:navigate subscription when the mediator changes. */
81
+ /** Set the mediator. If listeners are active, the `router:navigate` listener moves from the old one to the new one. */
53
82
  set mediator(value) {
54
83
  if (value === this.currentMediator) {
55
84
  return;
@@ -64,8 +93,9 @@ class Router {
64
93
  this.boundMediator = value;
65
94
  }
66
95
  }
96
+ /** Element or document whose `data-pushstate` link clicks this router handles; `null` means the whole document. */
67
97
  get navigationRoot() { return this.currentNavigationRoot; }
68
- /** Scope progressive link interception and move an active click listener when the root changes. */
98
+ /** Set the click root. If listeners are active, the click listener moves to the new root. */
69
99
  set navigationRoot(value) {
70
100
  if (value === this.currentNavigationRoot) {
71
101
  return;
@@ -80,22 +110,22 @@ class Router {
80
110
  }
81
111
  this.currentNavigationRoot = value;
82
112
  }
83
- /** True when browser navigation APIs are available. */
113
+ /** True when `window` and `document` exist, i.e. browser-only effects should run. */
84
114
  isBrowserRuntime() {
85
115
  return typeof window !== 'undefined' && typeof document !== 'undefined';
86
116
  }
87
- /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
117
+ /** Origin used as the base for URL parsing: the page's origin in a browser, a dummy one on a server. */
88
118
  getOrigin() {
89
119
  return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';
90
120
  }
91
- /** Read the authoritative browser URL, or retain the supplied server URL. */
121
+ /** Current path, query, and hash: from `window.location` in a browser, or the last committed URL (default `/`) on a server. */
92
122
  getCurrentUrl() {
93
123
  if (!this.isBrowserRuntime()) {
94
124
  return this.url || '/';
95
125
  }
96
126
  return window.location.pathname + window.location.search + (window.location.hash || '');
97
127
  }
98
- /** Parse a navigation candidate without changing Router state. */
128
+ /** Parse a URL relative to the current page (or the dummy server origin). Returns `null` if it can't be parsed. */
99
129
  parseNavigationUrl(url) {
100
130
  try {
101
131
  const base = this.isBrowserRuntime() ? window.location.href || `${window.location.origin}/` : this.getOrigin();
@@ -105,7 +135,7 @@ class Router {
105
135
  return null;
106
136
  }
107
137
  }
108
- /** Normalize browser navigation to a same-origin path, query, and hash. */
138
+ /** In a browser, reduce a URL to its path, query, and hash, or `null` if it is cross-origin or invalid. */
109
139
  normalizeBrowserUrl(url) {
110
140
  if (!this.isBrowserRuntime()) {
111
141
  return url;
@@ -116,35 +146,81 @@ class Router {
116
146
  }
117
147
  return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
118
148
  }
119
- /** Return whether a selected route has runnable view behavior. */
149
+ /** True if the route has something to run: a function, a `view` function, or a `view.initialize`. */
120
150
  isRunnableRoute(route) {
121
151
  return typeof route === 'function' || typeof route.view === 'function' ||
122
152
  Boolean(route.view && typeof route.view.initialize === 'function');
123
153
  }
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}/`))) {
154
+ /** Build (or reuse) the lookup tables for the current route keys. Reused while the own key list is unchanged. */
155
+ compileRoutes() {
156
+ const keys = Object.keys(this.routes);
157
+ const cached = this.compiledRoutes;
158
+ if (cached && cached.keys.length === keys.length && cached.keys.every((key, index) => key === keys[index])) {
159
+ return cached;
160
+ }
161
+ const prefixes = new Map();
162
+ const prefixFor = new Map();
163
+ for (const key of keys) {
164
+ if (key === 'defaultRoute') {
129
165
  continue;
130
166
  }
131
- if (!candidateRoute || route.length > candidateRoute.length) {
132
- candidateRoute = route;
167
+ const prefix = normalizeRouteKey(key);
168
+ prefixFor.set(key, prefix);
169
+ const existing = prefixes.get(prefix);
170
+ // When two keys normalize to the same path, prefer the one already in normalized form, then the alphabetically
171
+ // lowest, so the winner never depends on insertion order.
172
+ if (existing === undefined || (existing !== prefix && (key === prefix || key < existing))) {
173
+ prefixes.set(prefix, key);
133
174
  }
134
175
  }
135
- if (!candidateRoute && Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {
176
+ this.compiledRoutes = { keys, prefixes, prefixFor };
177
+ return this.compiledRoutes;
178
+ }
179
+ /** Find the route key with the longest matching path prefix, else `'defaultRoute'` if one is set, else `null`. */
180
+ matchRoute(pathname) {
181
+ const { prefixes } = this.compileRoutes();
182
+ // Try the full path, then drop one '/segment' at a time; the first hit is the longest match.
183
+ // Cutting only at '/' means '/products' matches '/products/42' but not '/products-old'.
184
+ let candidateRoute = prefixes.get(pathname);
185
+ let index = pathname.lastIndexOf('/');
186
+ while (candidateRoute === undefined && index >= 0) {
187
+ candidateRoute = prefixes.get(pathname.slice(0, index));
188
+ index = index > 0 ? pathname.lastIndexOf('/', index - 1) : -1;
189
+ }
190
+ if (candidateRoute !== undefined) {
191
+ return candidateRoute;
192
+ }
193
+ if (Object.prototype.hasOwnProperty.call(this.routes, 'defaultRoute') && this.routes.defaultRoute) {
136
194
  return 'defaultRoute';
137
195
  }
138
- return candidateRoute;
196
+ return null;
197
+ }
198
+ /** Length of the normalized path for a route key, used to slice the matched prefix off `data.url`; 0 for `defaultRoute`. */
199
+ routePrefixLength(route) {
200
+ if (!route || route === 'defaultRoute') {
201
+ return 0;
202
+ }
203
+ return (this.compileRoutes().prefixFor.get(route) ?? normalizeRouteKey(route)).length;
139
204
  }
140
- /** Build decoded path, query, and mediator data for a navigation candidate. */
205
+ /** Parse a URL Router committed earlier. A path is prefixed with the origin first, so '//x' stays a path, not a host. */
206
+ parseStoredUrl(url) {
207
+ if (!url.startsWith('/')) {
208
+ return this.parseNavigationUrl(url);
209
+ }
210
+ try {
211
+ return new URL(`${this.getOrigin()}${url}`);
212
+ }
213
+ catch (_error) {
214
+ return null;
215
+ }
216
+ }
217
+ /** Build the location object passed to route callbacks. Path segments that fail to decode are kept as-is. */
141
218
  buildLocationData(url, route, mediatorData, parsedUrl) {
142
- const matchedRoute = route && route !== 'defaultRoute' ? route : '';
143
219
  return {
144
220
  url,
145
221
  data: {
146
222
  url: parsedUrl.pathname
147
- .slice(matchedRoute.length)
223
+ .slice(this.routePrefixLength(route))
148
224
  .split('/')
149
225
  .filter(Boolean)
150
226
  .map((fragment) => {
@@ -161,15 +237,27 @@ class Router {
161
237
  };
162
238
  }
163
239
  /**
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.
240
+ * Attach listeners and dispatch the starting URL. Does not add a history entry or move focus.
241
+ * @param url - Request URL on a server. In a browser, omit it to use `window.location`.
242
+ * @returns This router.
243
+ * @throws Whatever a guard or route callback throws.
166
244
  */
167
245
  initialize(url) {
168
246
  this.addListeners();
169
- this.navigate(url ?? this.getCurrentUrl(), {}, true);
247
+ // The browser has already placed focus on the page it just loaded, so only later navigations move it.
248
+ this.initialDispatch = true;
249
+ try {
250
+ this.navigate(url ?? this.getCurrentUrl(), {}, true);
251
+ }
252
+ finally {
253
+ this.initialDispatch = false;
254
+ }
170
255
  return this;
171
256
  }
172
- /** Register browser and optional mediator handlers once. */
257
+ /**
258
+ * Attach the click and popstate listeners (browser only) and the mediator listener. Safe to call more than once.
259
+ * @returns This router.
260
+ */
173
261
  addListeners() {
174
262
  if (this.listenersInitialized) {
175
263
  return this;
@@ -187,12 +275,18 @@ class Router {
187
275
  this.listenersInitialized = true;
188
276
  return this;
189
277
  }
190
- /** Release owned state and listeners so the instance can leave the application lifecycle. */
278
+ /**
279
+ * Remove this router's listeners. Does not call the active route's `destroy()`.
280
+ * @returns This router.
281
+ */
191
282
  destroy() {
192
283
  this.removeListeners();
193
284
  return this;
194
285
  }
195
- /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
286
+ /**
287
+ * Remove the listeners `addListeners()` attached. Subclasses can override this to release more.
288
+ * @returns This router.
289
+ */
196
290
  removeListeners() {
197
291
  if (!this.listenersInitialized) {
198
292
  return this;
@@ -209,7 +303,15 @@ class Router {
209
303
  this.listenersInitialized = false;
210
304
  return this;
211
305
  }
212
- /** Intercept eligible same-origin push-state links while preserving normal browser actions. */
306
+ /**
307
+ * Click handler for `<a href data-pushstate>` links. Takes over only when Router can handle the link:
308
+ * a plain left click on a same-origin link (no `download`, no other `target`) whose route is app-defined,
309
+ * runnable, and allowed by its guard, and that is not just a `#fragment` of the current page. Anything else is left
310
+ * to the browser.
311
+ * @param e - The click event.
312
+ * @returns This router if it handled the click, otherwise `true`.
313
+ * @throws Whatever a guard or route callback throws.
314
+ */
213
315
  eventPushStateClick(e) {
214
316
  if (typeof window === 'undefined') {
215
317
  return true;
@@ -227,7 +329,8 @@ class Router {
227
329
  if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {
228
330
  return true;
229
331
  }
230
- const href = anchor.href || anchor.getAttribute('href') || '';
332
+ // Read the attribute, because on an SVG <a> the href property is an SVGAnimatedString, not a URL.
333
+ const href = anchor.getAttribute('href') ?? '';
231
334
  const baseUrl = typeof document !== 'undefined' && document.baseURI
232
335
  ? document.baseURI
233
336
  : window.location.href || `${window.location.origin}/`;
@@ -241,41 +344,74 @@ class Router {
241
344
  if (location.origin !== window.location.origin) {
242
345
  return true;
243
346
  }
347
+ // A link to a #fragment of the page already shown is native fragment navigation: let the browser scroll to it.
348
+ // Its popstate then only updates `url`, matching the fragment-only back/forward rule.
349
+ if (location.href.includes('#') && location.pathname === window.location.pathname && location.search === window.location.search) {
350
+ return true;
351
+ }
244
352
  const candidateRoute = this.matchRoute(location.pathname);
245
353
  const selected = candidateRoute ? this.routes[candidateRoute] : undefined;
246
- if (!selected || !this.isRunnableRoute(selected)) {
354
+ if (!selected || !this.isRunnableRoute(selected) || builtInDefaultRoutes.has(selected)) {
247
355
  return true;
248
356
  }
249
- e.preventDefault();
250
- this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false);
251
- return this;
357
+ // Guards are synchronous, so try the navigation first and cancel the browser's default only if Router took it.
358
+ // A rejected (or throwing) guard therefore falls back to a normal page load. If the route committed and then
359
+ // threw, the token has moved, so the default is still cancelled and the browser doesn't navigate twice.
360
+ const token = this.navigationToken;
361
+ let handled = false;
362
+ try {
363
+ handled = this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false) !== false;
364
+ }
365
+ finally {
366
+ if (handled || this.navigationToken !== token) {
367
+ e.preventDefault();
368
+ }
369
+ }
370
+ return handled ? this : true;
252
371
  }
253
- /** Restore browser history state without adding a new history entry. */
372
+ /**
373
+ * Popstate handler for back/forward. Runs the route for the new URL without adding a history entry.
374
+ * A hash-only change just updates `url` and `locationData.url`.
375
+ * @returns This router.
376
+ */
254
377
  eventPopState() {
255
378
  if (typeof window === 'undefined') {
256
379
  return this;
257
380
  }
258
- this.navigate(this.getCurrentUrl(), {}, true);
381
+ const currentUrl = this.getCurrentUrl();
382
+ // Moving between #fragments on the same page (footnotes, skip links) keeps the current view and focus.
383
+ if (this.route !== null && withoutHash(currentUrl) === withoutHash(this.url)) {
384
+ this.url = currentUrl;
385
+ this.locationData = { ...this.locationData, url: currentUrl };
386
+ return this;
387
+ }
388
+ this.navigate(currentUrl, {}, true);
259
389
  return this;
260
390
  }
261
- /** Decode URL query parameters, keeping the last value for duplicate keys. */
391
+ /**
392
+ * Decode a query string into a plain object. For repeated keys the last value wins.
393
+ * @param queryString - Query string, with or without the leading `?`.
394
+ */
262
395
  parseQueryString(queryString) {
263
396
  return Object.fromEntries(new URLSearchParams(queryString));
264
397
  }
265
- /** Build decoded path, query, and mediator data for the selected route. */
398
+ /**
399
+ * Rebuild `locationData` from the current `url` and `route`. Leaves it unchanged if the URL can't be parsed.
400
+ * @param mediatorData - Value to expose as `locationData.data.mediator`.
401
+ */
266
402
  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) {
403
+ const parsedUrl = this.parseStoredUrl(this.url || '/');
404
+ if (!parsedUrl) {
274
405
  return;
275
406
  }
276
407
  this.locationData = this.buildLocationData(this.url, this.route, mediatorData, parsedUrl);
277
408
  }
278
- /** Update page context after a route renders. DOM effects are browser-only. */
409
+ /**
410
+ * Record the route's title in `pageTitle` and, in a browser, set `document.title` and move focus to the route's
411
+ * `focus` selector (default `main h1`), adding `tabindex="-1"` if needed. `focus: false` skips focus.
412
+ * @param route - The route that just ran.
413
+ * @returns This router.
414
+ */
279
415
  applyPageContext(route) {
280
416
  if (typeof route === 'function') {
281
417
  this.pageTitle = null;
@@ -288,7 +424,7 @@ class Router {
288
424
  if (this.pageTitle) {
289
425
  document.title = this.pageTitle;
290
426
  }
291
- if (route.focus !== false) {
427
+ if (route.focus !== false && !this.suppressFocus) {
292
428
  const target = document.querySelector(route.focus || 'main h1');
293
429
  if (target) {
294
430
  if (!target.hasAttribute('tabindex')) {
@@ -299,25 +435,39 @@ class Router {
299
435
  }
300
436
  return this;
301
437
  }
302
- /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
438
+ /**
439
+ * Navigate to a URL: match the route, run its guard, commit the new state, call the previous route's `destroy()`,
440
+ * run the new route, apply title and focus, then push a history entry (browser only, unless `isPopState`).
441
+ * If route code starts another navigation part-way through, that one wins and this call stops early.
442
+ * @param url - Destination URL. Omit to re-run the last committed URL (or the current browser URL / `/` at first).
443
+ * @param mediatorData - Value exposed to the route as `location.data.mediator`.
444
+ * @param isPopState - `true` to skip adding a history entry.
445
+ * @returns This router, or `false` if the navigation was rejected (bad or cross-origin URL, no matching route,
446
+ * guard refused, or nothing to run). A rejected navigation changes nothing.
447
+ * @throws Whatever a guard or route callback throws. A throwing guard changes nothing; a throwing route stays committed.
448
+ */
303
449
  navigate(url, mediatorData, isPopState = false) {
450
+ const skipFocus = this.initialDispatch;
451
+ this.initialDispatch = false;
304
452
  if (url !== undefined && typeof url !== 'string') {
305
453
  return false;
306
454
  }
307
- let candidateInput = url !== undefined ? url : this.url;
455
+ const candidateInput = url !== undefined ? url : this.url;
456
+ let parsedUrl;
308
457
  if (!candidateInput) {
309
- candidateInput = this.getCurrentUrl();
458
+ parsedUrl = this.parseNavigationUrl(this.getCurrentUrl());
459
+ }
460
+ else {
461
+ parsedUrl = url === undefined ? this.parseStoredUrl(candidateInput) : this.parseNavigationUrl(candidateInput);
310
462
  }
311
- const parsedUrl = this.parseNavigationUrl(candidateInput);
312
463
  if (!parsedUrl) {
313
464
  return false;
314
465
  }
315
466
  if (this.isBrowserRuntime() && parsedUrl.origin !== window.location.origin) {
316
467
  return false;
317
468
  }
318
- const candidateUrl = this.isBrowserRuntime()
319
- ? `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`
320
- : candidateInput;
469
+ // Store only path, query, and hash, even when a server passed an absolute URL.
470
+ const candidateUrl = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
321
471
  const candidateRoute = this.matchRoute(parsedUrl.pathname);
322
472
  if (!candidateRoute) {
323
473
  return false;
@@ -328,34 +478,54 @@ class Router {
328
478
  !this.isRunnableRoute(selected)) {
329
479
  return false;
330
480
  }
481
+ const token = ++this.navigationToken;
331
482
  this.url = candidateUrl;
332
483
  this.route = candidateRoute;
333
484
  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);
485
+ // Clear the stored teardown before calling it, so a navigation started inside destroy() can't call it again,
486
+ // and a destroy() that throws is not retried: its error propagates with the destination committed but not run.
487
+ const previous = this.activeDestroy;
488
+ this.activeDestroy = undefined;
489
+ if (previous) {
490
+ previous.callback.call(previous.receiver, this.scope, candidateLocationData);
491
+ if (token !== this.navigationToken) {
492
+ return this;
342
493
  }
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;
494
+ }
495
+ const selectedView = typeof selected !== 'function' && typeof selected.view === 'object'
496
+ ? selected.view
497
+ : undefined;
498
+ // Store the teardown before initialize() runs, so it still runs later if initialize() throws or navigates away.
499
+ this.activeDestroy = selectedView?.destroy
500
+ ? { receiver: selectedView, callback: selectedView.destroy }
501
+ : undefined;
502
+ if (typeof selected === 'function') {
503
+ selected(this.scope, candidateLocationData);
504
+ }
505
+ else if (typeof selected.view === 'function') {
506
+ selected.view(this.scope, candidateLocationData);
507
+ }
508
+ else {
509
+ selectedView.initialize(this.scope, candidateLocationData);
510
+ }
511
+ // If initialize() started another navigation, that one has already applied its own title, focus, and history.
512
+ if (token !== this.navigationToken) {
513
+ return this;
514
+ }
515
+ this.suppressFocus = skipFocus;
516
+ try {
355
517
  this.applyPageContext(selected);
356
- this.previousRoute = this.route;
357
518
  }
358
- if (!isPopState && this.isBrowserRuntime()) {
519
+ finally {
520
+ this.suppressFocus = false;
521
+ }
522
+ if (token !== this.navigationToken) {
523
+ return this;
524
+ }
525
+ this.previousRoute = this.route;
526
+ // Re-running the current URL (no `url` given, e.g. a mediator event without a destination) must not add a
527
+ // duplicate history entry for the address already shown.
528
+ if (!isPopState && this.isBrowserRuntime() && !(url === undefined && parsedUrl.href === window.location.href)) {
359
529
  window.history.pushState(this.url, this.pageTitle || '', parsedUrl.href);
360
530
  }
361
531
  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 // 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 /** 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 keys = Object.keys(this.routes);\n const cached = this.compiledRoutes;\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.2.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
67
  "@babel/core": "8.0.5",
68
68
  "@babel/eslint-parser": "8.0.5",
69
69
  "@babel/preset-typescript": "8.0.1",
70
70
  "@eslint/js": "10.0.1",
71
- "eslint": "10.10.0"
71
+ "@types/node": "24.13.3",
72
+ "c8": "12.0.0",
73
+ "eslint": "10.10.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
  ".": {