white-label-router 4.0.0 → 5.0.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/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bshack
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,82 +1,129 @@
1
1
  # white-label-router
2
2
 
3
- `white-label-router` is a small client-side router built on the browser History API. It maps URL prefixes to functions or view lifecycle objects, intercepts links marked with `data-pushstate`, handles browser back/forward navigation, and can receive navigation requests through a mediator.
3
+ > Turn URLs into application intent without hiding the web platform.
4
+
5
+ `white-label-router` is a framework-independent TypeScript router for browser and server applications. It progressively enhances real links with the History API in the browser and dispatches explicit request URLs on the server through the same matching, guard, lifecycle, query, focus, and mediator contracts.
6
+
7
+ [Documentation](https://whitelabeljs.org/docs/router/) · [API reference](https://whitelabeljs.org/api/#router) · [Demo site](https://whitelabeljs.org/)
8
+
9
+ **Responsibility:** translate navigation into application intent. Nothing more.
10
+
11
+ ## Why it exists
12
+
13
+ White Label keeps URLs, links, history, and server requests visible rather than replacing them with a proprietary navigation model. Router adds the application boundary around those platform primitives while preserving normal browser behavior whenever enhancement does not apply.
14
+
15
+ Use it independently or compose it with the rest of White Label:
16
+
17
+ - [`white-label-mediator`](https://github.com/bshack/white-label-mediator) can publish `router:navigate` intent.
18
+ - [`white-label-model`](https://github.com/bshack/white-label-model) can own state changed by a route.
19
+ - [`white-label-view`](https://github.com/bshack/white-label-view) can own the rendering lifecycle started by a route.
20
+ - [`generator-white-label`](https://github.com/bshack/white-label) demonstrates the complete composition.
21
+
22
+ The package has no runtime dependency on the other White Label packages.
4
23
 
5
24
  ## Requirements
6
25
 
7
- - Node.js `^22.18.0` or `>=24.11.0` for installation and development
8
- - A browser environment with `window.history`, `window.location`, and standard DOM events at runtime
9
- - Server fallback configuration that serves the application entry page for client-managed URLs
26
+ - Node.js `^22.18.0` or `>=24.11.0` for installation, development, and server execution.
27
+ - npm, Yarn, and pnpm are supported for installation; see [`PACKAGE_MANAGERS.md`](PACKAGE_MANAGERS.md).
28
+ - Browser History API, location, and standard DOM events only when using browser navigation behavior.
29
+ - A directly requestable server route for every public browser route so enhanced links remain meaningful without JavaScript.
10
30
 
11
- ## Install and import
31
+ ## Install
12
32
 
13
33
  ```sh
14
34
  npm install white-label-router
35
+ # or: yarn add white-label-router
36
+ # or: pnpm add white-label-router
15
37
  ```
16
38
 
39
+ The same entrypoint is used in browsers and servers:
40
+
17
41
  ```js
18
42
  import Router from 'white-label-router';
19
43
  ```
20
44
 
21
- ## Complete example
45
+ ## One route contract, two runtimes
22
46
 
23
- ```js
24
- import Router from 'white-label-router';
47
+ ### Browser
25
48
 
26
- class ApplicationRouter extends Router {
27
- constructor() {
28
- super();
49
+ Omit the URL to dispatch the current browser location and attach browser navigation listeners:
29
50
 
30
- this.scope = document.querySelector('main');
31
- this.routes = {
32
- '/products': (scope, location) => {
33
- scope.textContent = `Product: ${location.data.url[0] || 'all'}`;
34
- },
35
- '/account': {
36
- title: 'Your account',
37
- secure: () => Boolean(window.currentUser),
38
- view: {
39
- initialize: (scope, location) => {
40
- scope.textContent = 'Account';
41
- console.log(location.data.query);
42
- },
43
- destroy: () => {
44
- console.log('Leaving account');
45
- }
46
- }
47
- },
48
- defaultRoute: () => {
49
- console.log('No configured route matched.');
50
- }
51
- };
52
- }
53
- }
51
+ ```js
52
+ const router = new Router();
53
+ router.routes = {
54
+ '/products': (_scope, location) => console.log(location.data.url),
55
+ defaultRoute: () => true
56
+ };
54
57
 
55
- const router = new ApplicationRouter();
58
+ // initialize() dispatches the current URL and returns the router for chaining.
56
59
  router.initialize();
57
60
  ```
58
61
 
59
- At startup, `initialize()` reads the current path and query string, registers document click and browser `popstate` listeners, and navigates to the matching route.
62
+ ### Server
63
+
64
+ Pass the request URL explicitly. No `window` or `document` is required:
65
+
66
+ ```js
67
+ const router = new Router();
68
+ router.routes = {
69
+ '/products': (_scope, location) => console.log(location.data.url),
70
+ defaultRoute: () => true
71
+ };
72
+ router.initialize('/products/42?color=blue');
73
+ ```
74
+
75
+ Browser-only history, click interception, focus, and document-title effects are skipped on the server. Scope mutable routing state per request when appropriate.
76
+
77
+ ## Serverless and function runtimes
78
+
79
+ Pass the incoming request URL explicitly when Router runs inside a serverless function. The server path uses the same matching, query parsing, guards, and route lifecycle without requiring `window` or `document`.
80
+
81
+ ```js
82
+ const requestUrl = new URL(request.url);
83
+ const router = new Router();
84
+ router.routes = routes;
85
+ router.initialize(`${requestUrl.pathname}${requestUrl.search}`);
86
+ ```
87
+
88
+ Create a request-scoped Router when its location state, mediator, route lifecycle, or other mutable configuration belongs to one invocation. Warm function processes may serve sequential or overlapping requests, so sharing one mutable Router can mix location/lifecycle state unless that process-wide lifetime is intentional.
89
+
90
+ Router handles **application routing inside the invocation**. It does not replace API Gateway routes, CDN routing, Vercel/Netlify/Cloudflare route configuration, load balancing, authentication infrastructure, or other cloud request plumbing.
91
+
92
+ The package currently documents Node.js as its supported server runtime. Its DOM-free server path uses standard URL APIs, but that is not a blanket compatibility claim for every edge provider; verify the actual target runtime before deployment.
93
+
94
+ ## Progressive enhancement first
95
+
96
+ Public navigation should remain a real link:
97
+
98
+ ```html
99
+ <a href="/products/42" data-pushstate>View product 42</a>
100
+ ```
101
+
102
+ `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.
103
+
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.
60
105
 
61
106
  ## Define routes
62
107
 
63
- A route may be a function:
108
+ A route can be a function:
64
109
 
65
110
  ```js
66
- this.routes = {
111
+ router.routes = {
67
112
  '/help': (scope, location) => {
68
113
  scope.textContent = `Help page: ${location.url}`;
69
114
  }
70
115
  };
71
116
  ```
72
117
 
73
- Or it may describe a title, security check, and view lifecycle:
118
+ Or an object describing route lifecycle:
74
119
 
75
120
  ```js
76
- this.routes = {
121
+ router.routes = {
77
122
  '/orders': {
78
123
  title: 'Orders',
124
+ focus: '#orders-title',
79
125
  secure(scope, location) {
126
+ // Guards must return exactly true to allow navigation.
80
127
  return Boolean(window.currentUser);
81
128
  },
82
129
  view: {
@@ -91,85 +138,65 @@ this.routes = {
91
138
  };
92
139
  ```
93
140
 
94
- The `secure` function must return exactly `true` to allow navigation. When navigation succeeds, the previous route's `destroy()` runs before the next route's `initialize()`.
141
+ `secure()` must return exactly `true` to allow navigation. On a successful route change, the previous route's `destroy()` runs before the next route's `initialize()`.
142
+
143
+ ## Matching
95
144
 
96
- ### Route matching behavior
145
+ Routes match complete path boundaries. `/products` matches `/products` and `/products/42`, but not `/products-old`.
97
146
 
98
- Routes match complete path boundaries in object insertion order. For example, `/products` matches `/products` and `/products/42`, but it does not match `/products-old`. Put more specific routes before broader ones:
147
+ When multiple route prefixes match, Router selects the **longest matching route**, so specific routes win without depending on object insertion order:
99
148
 
100
149
  ```js
101
- this.routes = {
102
- '/products/sale': saleRoute,
150
+ router.routes = {
103
151
  '/products': productsRoute,
152
+ '/products/sale': saleRoute,
104
153
  defaultRoute
105
154
  };
106
155
  ```
107
156
 
108
- If no prefix matches, `defaultRoute` is used when present.
109
-
110
- ## Navigate from HTML
111
-
112
- The router delegates one document-level click listener. Add `data-pushstate` to an anchor to use client-side navigation:
113
-
114
- ```html
115
- <a href="/products/42" data-pushstate>
116
- <span>View product 42</span>
117
- </a>
118
- ```
119
-
120
- Clicks on nested elements such as the `span` are resolved to the enclosing anchor. Links without `data-pushstate` keep their normal browser behavior.
121
-
122
- Modified clicks, non-left clicks, downloads, links targeting another browsing context, and cross-origin URLs also keep their normal browser behavior.
123
-
124
- ## Navigate from JavaScript
125
-
126
- ```js
127
- router.navigate('/products/42', {
128
- source: 'featured-products'
129
- });
130
- ```
131
-
132
- The second argument becomes `location.data.mediator`. Unless the call represents a browser `popstate`, successful navigation adds the URL to browser history.
157
+ `/products/sale` selects `saleRoute` even though `/products` was defined first. When no prefix matches, `defaultRoute` runs if configured.
133
158
 
134
- ## Location data
159
+ ## Location contract
135
160
 
136
- Every route receives the configured `scope` and a location object:
161
+ Routes receive the configured `scope` and a location object:
137
162
 
138
163
  ```js
139
164
  {
140
165
  url: '/products/42?color=blue',
141
166
  data: {
142
167
  url: ['42'],
143
- mediator: {
144
- source: 'featured-products'
145
- },
146
- query: {
147
- color: 'blue'
148
- }
168
+ mediator: {source: 'featured-products'},
169
+ query: {color: 'blue'}
149
170
  }
150
171
  }
151
172
  ```
152
173
 
153
- - `url` is the complete route URL.
154
- - `data.url` contains path segments after the matched route prefix.
155
- - `data.mediator` contains the object passed to `navigate()` or received from the mediator.
156
- - `data.query` contains parsed query values.
174
+ `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 the mediator. Values are URI-decoded with standard WHATWG URL APIs; applications must still validate untrusted values.
157
175
 
158
- Path segments and query values are URI-decoded with browser-native URL APIs. Application code must still validate values before using them.
176
+ ## Programmatic navigation
159
177
 
160
- ## Version 3 migration note
178
+ ```js
179
+ const result = router.navigate('/products/42', {
180
+ source: 'featured-products'
181
+ });
161
182
 
162
- Version 3 changes route matching from unrestricted string prefixes to path boundaries. A route such as `/page2` no longer matches `/page23`. Define `/page23` explicitly if the application previously depended on that behavior.
183
+ // `result` is the router on success or false when navigation is rejected.
184
+ ```
163
185
 
164
- ## Mediator navigation
186
+ In browsers, successful navigation updates history unless the call represents `popstate`. On servers it dispatches without History API effects.
165
187
 
166
- Assign an EventEmitter-compatible mediator before initialization:
188
+ Calling `navigate()` without a URL dispatches the current browser URL. In a non-browser runtime the safe default is `/`; server applications should normally supply the request URL explicitly.
189
+
190
+ ## Mediator integration
191
+
192
+ Assign any EventEmitter-compatible mediator that provides `on()` and `removeListener()`:
167
193
 
168
194
  ```js
169
195
  import Mediator from 'white-label-mediator';
170
196
 
171
197
  const mediator = new Mediator();
172
- const router = new ApplicationRouter();
198
+ const router = new Router();
199
+
173
200
  router.mediator = mediator;
174
201
  router.initialize();
175
202
 
@@ -179,68 +206,78 @@ mediator.emit('router:navigate', {
179
206
  });
180
207
  ```
181
208
 
182
- The complete emitted object is passed to the route as `location.data.mediator`. `destroy()` removes the document, window, and mediator listeners. Repeated listener initialization is idempotent and does not add duplicate listeners.
209
+ The complete event object becomes `location.data.mediator`. `destroy()` removes listeners owned by Router. Repeated listener initialization is idempotent.
210
+
211
+ ## Public API
183
212
 
184
- ## Lifecycle
213
+ | Member | Behavior | Returns |
214
+ | --- | --- | --- |
215
+ | `routes` | Ordered route table of functions or lifecycle route objects. | Configuration property; not a method. |
216
+ | `scope` | Application scope passed to route callbacks. | Configuration property; not a method. |
217
+ | `mediator` | Optional EventEmitter-compatible source for `router:navigate`. | Configuration property; not a method. |
218
+ | `initialize(url?)` | Dispatch the browser URL or an explicit server URL and attach applicable listeners. | The same `Router` instance. |
219
+ | `navigate(url?, data?, isPopState?)` | Match and run a route; update browser history when appropriate. | The same `Router` instance on success; `false` for cross-origin browser URLs, rejected guards, or routes without runnable view behavior. |
220
+ | `addListeners()` | Attach browser and optional mediator listeners once. | The same `Router` instance. |
221
+ | `removeListeners()` | Release listeners owned by this router. | The same `Router` instance. |
222
+ | `destroy()` | Release routing listeners. | The same `Router` instance after cleanup. |
223
+ | `parseQueryString(query)` | Decode a query string using `URLSearchParams`. | A plain object containing the last value for each query key. |
224
+ | `setLocationData(data?)` | Rebuild the current route location payload. | `undefined`; updates `locationData` in place. |
225
+ | `applyPageContext(route)` | Update route title/focus context when applicable. | The same `Router` instance. |
226
+ | `eventPushStateClick(event)` | Handle an eligible browser push-state click. | The same `Router` instance after handled navigation; `true` when native/default handling should continue. |
227
+ | `eventPopState()` | Dispatch the current browser history location. | The same `Router` instance. |
185
228
 
186
- | Method | Behavior |
187
- | --- | --- |
188
- | `initialize()` | Reads the browser URL, adds listeners, and runs the initial route. |
189
- | `navigate(url, data, isPopState)` | Matches and runs a route, updates history, and returns the router; returns `false` when blocked or invalid. |
190
- | `addListeners()` | Adds browser and optional mediator listeners once. |
191
- | `removeListeners()` | Removes listeners previously added by this router. |
192
- | `destroy()` | Calls `removeListeners()` and returns the router. |
229
+ ## Title and focus
193
230
 
194
- Call `destroy()` when the router is no longer used:
231
+ Object routes can define browser title and focus behavior:
195
232
 
196
233
  ```js
197
- router.destroy();
234
+ router.routes = {
235
+ '/account': {
236
+ title: 'Account',
237
+ focus: '#account-title',
238
+ view: renderAccount
239
+ }
240
+ };
198
241
  ```
199
242
 
200
- ## Development
243
+ 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.
201
244
 
202
- ```sh
203
- npm ci
204
- npm run build
205
- npm run typecheck
206
- npm test
207
- npm run coverage
208
- npm run audit
209
- ```
245
+ ## Browser history behavior
210
246
 
211
- The npm package publishes the compiled `dist` file and this README.
247
+ 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.
212
248
 
213
- ## TypeScript development and version 4.0.0 migration
249
+ ## TypeScript
214
250
 
215
- Implementation code now uses strict TypeScript. Builds emit JavaScript, source maps with embedded source, and `.d.ts` declarations into `dist`. JavaScript callers can still use the package without compiling TypeScript themselves. JSDoc comments describe parameters, return values, lifecycle behavior, and validation at the implementation, and are retained in declarations.
251
+ Implementation uses strict TypeScript and emits JavaScript, source maps, and declarations into `dist`.
216
252
 
217
253
  ```ts
218
- import Router from 'white-label-router';
219
-
220
254
  const router = new Router();
221
255
  const greeting: Router.Route = (scope, location) => {
222
- console.log(location.data.query.name); // /hello?name=Ada
256
+ console.log(location.data.query.name);
223
257
  };
258
+
224
259
  router.routes['/hello'] = greeting;
225
260
  router.initialize();
226
261
  ```
227
262
 
228
- `Router.Route` covers function routes and objects with a guard or view lifecycle. `Router.Location` describes decoded URL data, `Router.Navigation` describes mediator payloads, and `Router.Handler` describes a route callback. Browser DOM types are required when compiling a consuming application.
229
-
230
- This is a major release because the distribution is now CommonJS emitted by TypeScript, replacing the previous UMD wrapper. CommonJS `require` and the documented ESM imports remain supported. Direct AMD loading or browser script tags that depended on UMD globals must migrate to a browser bundler. Edit `src/*.ts`, then run `npm run build`; do not edit generated `dist` files. The obsolete Babel build dependencies have been removed.
263
+ `Router.Route`, `Router.Location`, `Router.Navigation`, and `Router.Handler` expose the supported contracts. Browser DOM types are present because the same class supports browser scopes and events; server hosts do not need DOM globals at runtime.
231
264
 
232
- ### Verification and coverage
265
+ ## Development
233
266
 
234
267
  ```sh
235
268
  npm ci --ignore-scripts
269
+ npm run lint
236
270
  npm run typecheck
237
271
  npm test
238
272
  npm run coverage
273
+ npm run audit
239
274
  npm pack --dry-run
240
275
  ```
241
276
 
242
- `npm test` builds the code, checks TypeScript consumer examples against the emitted declarations, and runs the tests. `npm run coverage` additionally enforces **100% statements, branches, functions, and lines for each implementation file**. Unexecuted implementation files count toward the result; declaration-only files contain no executable code and are excluded. Reports are written to `coverage`, including `lcov.info` for coverage viewers. CI runs the same gate and checks committed build output for drift.
277
+ Tests cover browser behavior, DOM-free server routing, and parity of the shared route contract. Coverage enforces 100% statements, branches, functions, and lines per implementation file. CI builds authored source, uploads generated artifacts for inspection, audits dependencies, packs the package, and verifies the packed public API across npm, Yarn, and pnpm.
278
+
279
+ Edit `src/*.ts` and regenerate `dist`; do not edit generated files directly.
243
280
 
244
- Tests exercise the compiled JavaScript interface used by downstream callers. Coverage is an execution metric, not proof that all possible inputs or external integrations are correct.
281
+ ## Design boundary
245
282
 
246
- To undo this migration, revert its commit and run `npm ci` from the restored lockfile. No npm release, database migration, or production deployment is performed by these development changes.
283
+ Router owns URL-to-intent translation. It intentionally does not own application state, rendering, data loading, authentication policy, or server infrastructure. Real links and server URLs remain the foundation; Router enhances them instead of replacing them.
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ type RouteHandler = (scope: Element | null, location: LocationData) => unknown;
16
16
  /** An object route can own a function or a view lifecycle. */
17
17
  interface RouteObject {
18
18
  title?: string;
19
+ focus?: string | false;
19
20
  secure?: RouteHandler;
20
21
  view?: RouteHandler | {
21
22
  initialize?: RouteHandler;
@@ -26,9 +27,9 @@ type Route = RouteHandler | RouteObject;
26
27
  /** Minimal event-bus contract required by the router. */
27
28
  interface NavigationMediator {
28
29
  on(event: string, callback: (data?: NavigationData) => unknown): unknown;
29
- removeListener?(event: string, callback: (data?: NavigationData) => unknown): unknown;
30
+ removeListener(event: string, callback: (data?: NavigationData) => unknown): unknown;
30
31
  }
31
- /** History API router with ordered path-boundary matching. */
32
+ /** History API router that also dispatches request URLs in non-browser runtimes. */
32
33
  declare class Router {
33
34
  routes: Record<string, Route>;
34
35
  url: string;
@@ -42,64 +43,41 @@ declare class Router {
42
43
  boundMediatorNavigate: (data?: NavigationData) => unknown;
43
44
  listenersInitialized: boolean;
44
45
  locationData: LocationData;
45
- /**
46
- * Create an instance with its own state and listener references.
47
- */
46
+ /** Create an instance with its own state and listener references. */
48
47
  constructor();
48
+ /** True when browser navigation APIs are available. */
49
+ private isBrowserRuntime;
50
+ /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
51
+ private getOrigin;
52
+ /** Read the authoritative browser URL, or retain the supplied server URL. */
53
+ private getCurrentUrl;
54
+ /** Normalize browser navigation to a same-origin path, query, and hash. */
55
+ private normalizeBrowserUrl;
49
56
  /**
50
57
  * Start this instance and return it for lifecycle chaining.
51
- * @returns This instance for chaining.
52
- */
53
- initialize(): this;
54
- /**
55
- * Register stable document, history, and optional mediator handlers once.
56
- * @returns This instance for chaining.
58
+ * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
57
59
  */
60
+ initialize(url?: string): this;
61
+ /** Register browser and optional mediator handlers once. */
58
62
  addListeners(): this;
59
- /**
60
- * Release owned state and listeners so the instance can leave the application lifecycle.
61
- * @returns This instance after cleanup.
62
- */
63
+ /** Release owned state and listeners so the instance can leave the application lifecycle. */
63
64
  destroy(): this;
64
- /**
65
- * Release listeners owned by this instance; subclasses may extend the lifecycle hook.
66
- * @returns This instance for chaining.
67
- */
65
+ /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
68
66
  removeListeners(): this;
69
- /**
70
- * Intercept eligible same-origin push-state links while preserving normal browser actions.
71
- * @param e - Browser event being handled.
72
- * @returns True when the browser should handle the event, or this router after interception.
73
- */
67
+ /** Intercept eligible same-origin push-state links while preserving normal browser actions. */
74
68
  eventPushStateClick(e: MouseEvent): true | this;
75
- /**
76
- * Restore browser history state without adding a new history entry.
77
- * @param e - Browser event being handled.
78
- * @returns This router after restoring the history state.
79
- */
80
- eventPopState(e: PopStateEvent): this;
81
- /**
82
- * Decode URL query parameters, keeping the last value for duplicate keys.
83
- * @param queryString - Encoded URL query portion.
84
- * @returns A record of decoded query names and their final values.
85
- */
69
+ /** Restore browser history state without adding a new history entry. */
70
+ eventPopState(): this;
71
+ /** Decode URL query parameters, keeping the last value for duplicate keys. */
86
72
  parseQueryString(queryString: string): {
87
73
  [k: string]: string;
88
74
  };
89
- /**
90
- * Build decoded path, query, and mediator data for the selected route.
91
- * @param mediatorData - Optional caller metadata passed to the route.
92
- * @returns No value; locationData is updated in place.
93
- */
75
+ /** Build decoded path, query, and mediator data for the selected route. */
94
76
  setLocationData(mediatorData?: NavigationData): void;
95
- /**
96
- * Select a route, enforce its guard, transition view lifecycles, and update history and title.
97
- * @param url - URL or optional adapter argument.
98
- * @param mediatorData - Optional caller metadata passed to the route.
99
- * @param isPopState - Whether navigation came from browser history.
100
- * @returns False when a guard or route rejects navigation; otherwise this router.
101
- */
102
- navigate(url?: string | false, mediatorData?: NavigationData, isPopState?: boolean): false | this;
77
+ /** Update page context after a route renders. DOM effects are browser-only. */
78
+ applyPageContext(route: Route): this;
79
+ /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
80
+ navigate(url?: string, mediatorData?: NavigationData, isPopState?: boolean): false | this;
103
81
  }
104
82
  /** Public route configuration and navigation payload types. */
105
83
  declare namespace Router {
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
- /** History API router with ordered path-boundary matching. */
2
+ /** History API router that also dispatches request URLs in non-browser runtimes. */
3
3
  class Router {
4
4
  routes;
5
5
  url;
@@ -13,36 +13,22 @@ class Router {
13
13
  boundMediatorNavigate;
14
14
  listenersInitialized;
15
15
  locationData = { url: '', data: { url: [], mediator: undefined, query: {} } };
16
- /**
17
- * Create an instance with its own state and listener references.
18
- */
16
+ /** Create an instance with its own state and listener references. */
19
17
  constructor() {
20
- // placeholder for routes defined when extended
21
18
  this.routes = {
22
19
  defaultRoute: {
23
20
  view: {
24
- initialize: () => {
25
- return true;
26
- },
27
- destroy: () => {
28
- return true;
29
- }
21
+ initialize: () => true,
22
+ destroy: () => true
30
23
  }
31
24
  }
32
25
  };
33
- // this will hold the current url
34
26
  this.url = '';
35
- // this holds the current route
36
27
  this.route = null;
37
- // can house a container for the router to act upon
38
28
  this.scope = null;
39
- // this holds the previous route for destorying later
40
29
  this.previousRoute = null;
41
- // this will hold the page title (not yet supported by browsers)
42
30
  this.pageTitle = null;
43
- // optional mediator placeholder
44
31
  this.mediator = false;
45
- // Stable listener references allow destroy() to release browser resources.
46
32
  this.boundPushStateClick = this.eventPushStateClick.bind(this);
47
33
  this.boundPopState = this.eventPopState.bind(this);
48
34
  this.boundMediatorNavigate = (data = {}) => {
@@ -50,86 +36,93 @@ class Router {
50
36
  };
51
37
  this.listenersInitialized = false;
52
38
  }
39
+ /** True when browser navigation APIs are available. */
40
+ isBrowserRuntime() {
41
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
42
+ }
43
+ /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
44
+ getOrigin() {
45
+ return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';
46
+ }
47
+ /** Read the authoritative browser URL, or retain the supplied server URL. */
48
+ getCurrentUrl() {
49
+ if (!this.isBrowserRuntime()) {
50
+ return this.url || '/';
51
+ }
52
+ return window.location.pathname + window.location.search + (window.location.hash || '');
53
+ }
54
+ /** Normalize browser navigation to a same-origin path, query, and hash. */
55
+ normalizeBrowserUrl(url) {
56
+ if (!this.isBrowserRuntime()) {
57
+ return url;
58
+ }
59
+ const parsedUrl = new URL(url || '/', window.location.href || `${window.location.origin}/`);
60
+ if (parsedUrl.origin !== window.location.origin) {
61
+ return null;
62
+ }
63
+ return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
64
+ }
53
65
  /**
54
66
  * Start this instance and return it for lifecycle chaining.
55
- * @returns This instance for chaining.
67
+ * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
56
68
  */
57
- initialize() {
58
- //get the url fragment w/query string
59
- this.url = window.location.pathname + window.location.search;
60
- // set up the events
69
+ initialize(url) {
70
+ this.url = url ?? this.getCurrentUrl();
61
71
  this.addListeners();
62
- // navigate to correct view
63
- this.navigate(false, {}, false);
72
+ this.navigate(undefined, {}, true);
64
73
  return this;
65
74
  }
66
- /**
67
- * Register stable document, history, and optional mediator handlers once.
68
- * @returns This instance for chaining.
69
- */
75
+ /** Register browser and optional mediator handlers once. */
70
76
  addListeners() {
71
77
  if (this.listenersInitialized) {
72
78
  return this;
73
79
  }
74
- // Delegate push-state links from one document listener.
75
- document.addEventListener('click', this.boundPushStateClick);
76
- //bind window popstates
77
- window.addEventListener('popstate', this.boundPopState);
78
- //listen to a mediator if present
80
+ if (this.isBrowserRuntime()) {
81
+ document.addEventListener('click', this.boundPushStateClick);
82
+ window.addEventListener('popstate', this.boundPopState);
83
+ }
79
84
  if (this.mediator) {
80
85
  this.mediator.on('router:navigate', this.boundMediatorNavigate);
81
86
  }
82
87
  this.listenersInitialized = true;
83
88
  return this;
84
89
  }
85
- /**
86
- * Release owned state and listeners so the instance can leave the application lifecycle.
87
- * @returns This instance after cleanup.
88
- */
90
+ /** Release owned state and listeners so the instance can leave the application lifecycle. */
89
91
  destroy() {
90
92
  this.removeListeners();
91
93
  return this;
92
94
  }
93
- /**
94
- * Release listeners owned by this instance; subclasses may extend the lifecycle hook.
95
- * @returns This instance for chaining.
96
- */
95
+ /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
97
96
  removeListeners() {
98
97
  if (!this.listenersInitialized) {
99
98
  return this;
100
99
  }
101
- // Unbind the same stable listener objects registered by addListeners().
102
- document.removeEventListener('click', this.boundPushStateClick);
103
- //bind window popstates
104
- window.removeEventListener('popstate', this.boundPopState);
105
- if (this.mediator && typeof this.mediator.removeListener === 'function') {
100
+ if (this.isBrowserRuntime()) {
101
+ document.removeEventListener('click', this.boundPushStateClick);
102
+ window.removeEventListener('popstate', this.boundPopState);
103
+ }
104
+ if (this.mediator) {
106
105
  this.mediator.removeListener('router:navigate', this.boundMediatorNavigate);
107
106
  }
108
107
  this.listenersInitialized = false;
109
108
  return this;
110
109
  }
111
- /**
112
- * Intercept eligible same-origin push-state links while preserving normal browser actions.
113
- * @param e - Browser event being handled.
114
- * @returns True when the browser should handle the event, or this router after interception.
115
- */
110
+ /** Intercept eligible same-origin push-state links while preserving normal browser actions. */
116
111
  eventPushStateClick(e) {
117
- if (e.defaultPrevented ||
118
- (e.button !== undefined && e.button !== 0) ||
119
- e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
112
+ if (typeof window === 'undefined') {
113
+ return true;
114
+ }
115
+ if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
120
116
  return true;
121
117
  }
122
118
  const eventTarget = e.target;
123
119
  const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);
124
- const anchor = source && typeof source.closest === 'function'
125
- ? source.closest('a[href][data-pushstate]')
126
- : null;
120
+ const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;
127
121
  if (!anchor) {
128
122
  return true;
129
123
  }
130
124
  const target = anchor.getAttribute('target');
131
- if (anchor.hasAttribute('download') ||
132
- (target && target.toLowerCase() !== '_self')) {
125
+ if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {
133
126
  return true;
134
127
  }
135
128
  const location = new URL(anchor.getAttribute('href') || '', window.location.href || `${window.location.origin}/`);
@@ -137,47 +130,31 @@ class Router {
137
130
  return true;
138
131
  }
139
132
  e.preventDefault();
140
- // closest() supports nested elements at any depth inside the selected anchor.
141
133
  this.url = `${location.pathname}${location.search}${location.hash}`;
142
- this.navigate(false, {}, false);
134
+ this.navigate(undefined, {}, false);
143
135
  return this;
144
136
  }
145
- /**
146
- * Restore browser history state without adding a new history entry.
147
- * @param e - Browser event being handled.
148
- * @returns This router after restoring the history state.
149
- */
150
- eventPopState(e) {
151
- this.url = (e.state || '');
152
- this.navigate(false, {}, true);
137
+ /** Restore browser history state without adding a new history entry. */
138
+ eventPopState() {
139
+ if (typeof window === 'undefined') {
140
+ return this;
141
+ }
142
+ this.url = this.getCurrentUrl();
143
+ this.navigate(undefined, {}, true);
153
144
  return this;
154
145
  }
155
- /**
156
- * Decode URL query parameters, keeping the last value for duplicate keys.
157
- * @param queryString - Encoded URL query portion.
158
- * @returns A record of decoded query names and their final values.
159
- */
146
+ /** Decode URL query parameters, keeping the last value for duplicate keys. */
160
147
  parseQueryString(queryString) {
161
148
  return Object.fromEntries(new URLSearchParams(queryString));
162
149
  }
163
- /**
164
- * Build decoded path, query, and mediator data for the selected route.
165
- * @param mediatorData - Optional caller metadata passed to the route.
166
- * @returns No value; locationData is updated in place.
167
- */
150
+ /** Build decoded path, query, and mediator data for the selected route. */
168
151
  setLocationData(mediatorData) {
169
- const parsedUrl = new URL(this.url, window.location.origin);
152
+ const parsedUrl = new URL(this.url || '/', this.getOrigin());
170
153
  const matchedRoute = this.route && this.route !== 'defaultRoute' ? this.route : '';
171
- // this is the object passed to the matching view
172
154
  this.locationData = {
173
155
  url: this.url,
174
- data: {
175
- url: [],
176
- mediator: mediatorData,
177
- query: {}
178
- }
156
+ data: { url: [], mediator: mediatorData, query: {} }
179
157
  };
180
- // parse out the url data
181
158
  this.locationData.data.url = parsedUrl.pathname
182
159
  .slice(matchedRoute.length)
183
160
  .split('/')
@@ -186,48 +163,66 @@ class Router {
186
163
  try {
187
164
  return decodeURIComponent(fragment);
188
165
  }
189
- catch (error) {
166
+ catch (_error) {
190
167
  return fragment;
191
168
  }
192
169
  });
193
- //add query string data
194
170
  this.locationData.data.query = this.parseQueryString(parsedUrl.search);
195
171
  }
196
- /**
197
- * Select a route, enforce its guard, transition view lifecycles, and update history and title.
198
- * @param url - URL or optional adapter argument.
199
- * @param mediatorData - Optional caller metadata passed to the route.
200
- * @param isPopState - Whether navigation came from browser history.
201
- * @returns False when a guard or route rejects navigation; otherwise this router.
202
- */
172
+ /** Update page context after a route renders. DOM effects are browser-only. */
173
+ applyPageContext(route) {
174
+ if (typeof route === 'function') {
175
+ this.pageTitle = null;
176
+ return this;
177
+ }
178
+ this.pageTitle = typeof route.title === 'string' ? route.title : null;
179
+ if (!this.isBrowserRuntime()) {
180
+ return this;
181
+ }
182
+ if (this.pageTitle) {
183
+ document.title = this.pageTitle;
184
+ }
185
+ if (route.focus !== false) {
186
+ const target = document.querySelector(route.focus || 'main h1');
187
+ if (target) {
188
+ if (!target.hasAttribute('tabindex')) {
189
+ target.setAttribute('tabindex', '-1');
190
+ }
191
+ target.focus();
192
+ }
193
+ }
194
+ return this;
195
+ }
196
+ /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
203
197
  navigate(url, mediatorData, isPopState = false) {
204
- // allow navigate to use a specified url
205
- if (url) {
198
+ if (url !== undefined) {
206
199
  this.url = url;
207
200
  }
208
- //clean up url for matching
209
- this.url = this.url.replace(window.location.origin, '');
210
- // reset this to null for new location
201
+ if (!this.url) {
202
+ this.url = this.getCurrentUrl();
203
+ }
204
+ const normalizedUrl = this.normalizeBrowserUrl(this.url);
205
+ if (normalizedUrl === null) {
206
+ return false;
207
+ }
208
+ this.url = normalizedUrl;
211
209
  this.route = null;
212
- const pathname = new URL(this.url, window.location.origin).pathname;
213
- // Match complete path segments so /page2 does not also match /page23.
214
- for (let route in this.routes) {
215
- if (route !== 'defaultRoute' &&
216
- (pathname === route || pathname.startsWith(`${route}/`))) {
210
+ const pathname = new URL(this.url, this.getOrigin()).pathname;
211
+ for (const route in this.routes) {
212
+ if (route === 'defaultRoute' || (pathname !== route && !pathname.startsWith(`${route}/`))) {
213
+ continue;
214
+ }
215
+ if (!this.route || route.length > this.route.length) {
217
216
  this.route = route;
218
- //stop looping we are done
219
- break;
220
217
  }
221
218
  }
222
219
  this.setLocationData(mediatorData);
223
- // if a catch all default route is defined execute that when not match is found
224
220
  if (!this.route && this.routes.defaultRoute) {
225
221
  this.route = 'defaultRoute';
226
222
  }
227
223
  if (this.route) {
228
224
  const selected = this.routes[this.route];
229
- if (typeof selected !== 'function' && selected.secure &&
230
- selected.secure(this.scope, this.locationData) !== true) {
225
+ if (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, this.locationData) !== true) {
231
226
  return false;
232
227
  }
233
228
  const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;
@@ -239,29 +234,21 @@ class Router {
239
234
  }
240
235
  else if (typeof selected.view === 'function') {
241
236
  selected.view(this.scope, this.locationData);
242
- this.pageTitle = typeof selected.title === 'string' ? selected.title : null;
243
237
  }
244
238
  else if (selected.view && typeof selected.view.initialize === 'function') {
245
- this.pageTitle = typeof selected.title === 'string' ? selected.title : null;
246
239
  selected.view.initialize(this.scope, this.locationData);
247
240
  }
248
241
  else {
249
242
  return false;
250
243
  }
244
+ this.applyPageContext(selected);
251
245
  this.previousRoute = this.route;
252
246
  }
253
- // make sure to not set pushstate on back button click
254
- if (!isPopState) {
255
- //set in history the new url
247
+ if (!isPopState && this.isBrowserRuntime()) {
256
248
  window.history.pushState(this.url, this.pageTitle || '', this.url);
257
249
  }
258
- // since browsers don't support setting the title with pushstate yet
259
- if (this.pageTitle) {
260
- document.title = this.pageTitle;
261
- }
262
250
  return this;
263
251
  }
264
252
  }
265
- ;
266
253
  module.exports = Router;
267
254
  //# sourceMappingURL=index.js.map
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;\n/** An object route can own a function or a view lifecycle. */\ninterface RouteObject {title?: string; secure?: RouteHandler; view?: RouteHandler | {initialize?: RouteHandler; destroy?: RouteHandler}}\ntype Route = RouteHandler | RouteObject;\n/** Minimal event-bus contract required by the router. */\ninterface NavigationMediator {\n on(event: string, callback: (data?: NavigationData) => unknown): unknown;\n removeListener?(event: string, callback: (data?: NavigationData) => unknown): unknown;\n}\n/** History API router with ordered path-boundary matching. */\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 mediator: NavigationMediator | false;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: (data?: NavigationData) => unknown;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n\n /**\n * Create an instance with its own state and listener references.\n */\n constructor() {\n\n // placeholder for routes defined when extended\n this.routes = {\n defaultRoute: {\n view: {\n initialize: () => {\n return true;\n },\n destroy: () => {\n return true;\n }\n }\n }\n }\n\n // this will hold the current url\n this.url = '';\n\n // this holds the current route\n this.route = null;\n\n // can house a container for the router to act upon\n this.scope = null;\n\n // this holds the previous route for destorying later\n this.previousRoute = null;\n\n // this will hold the page title (not yet supported by browsers)\n this.pageTitle = null;\n\n // optional mediator placeholder\n this.mediator = false;\n\n // Stable listener references allow destroy() to release browser resources.\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (data = {}) => {\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n\n }\n\n /**\n * Start this instance and return it for lifecycle chaining.\n * @returns This instance for chaining.\n */\n initialize() {\n\n //get the url fragment w/query string\n this.url = window.location.pathname + window.location.search;\n\n // set up the events\n this.addListeners();\n\n // navigate to correct view\n this.navigate(false, {}, false);\n\n return this;\n\n }\n\n /**\n * Register stable document, history, and optional mediator handlers once.\n * @returns This instance for chaining.\n */\n addListeners() {\n\n if (this.listenersInitialized) {\n return this;\n }\n\n // Delegate push-state links from one document listener.\n document.addEventListener('click', this.boundPushStateClick);\n\n //bind window popstates\n window.addEventListener('popstate', this.boundPopState);\n\n //listen to a mediator if present\n if (this.mediator) {\n this.mediator.on('router:navigate', this.boundMediatorNavigate);\n }\n\n this.listenersInitialized = true;\n return this;\n\n }\n\n /**\n * Release owned state and listeners so the instance can leave the application lifecycle.\n * @returns This instance after cleanup.\n */\n destroy() {\n\n this.removeListeners();\n\n return this;\n\n }\n\n /**\n * Release listeners owned by this instance; subclasses may extend the lifecycle hook.\n * @returns This instance for chaining.\n */\n removeListeners() {\n\n if (!this.listenersInitialized) {\n return this;\n }\n\n // Unbind the same stable listener objects registered by addListeners().\n document.removeEventListener('click', this.boundPushStateClick);\n\n //bind window popstates\n window.removeEventListener('popstate', this.boundPopState);\n\n if (this.mediator && typeof this.mediator.removeListener === 'function') {\n this.mediator.removeListener('router:navigate', this.boundMediatorNavigate);\n }\n\n this.listenersInitialized = false;\n return this;\n\n }\n\n /**\n * Intercept eligible same-origin push-state links while preserving normal browser actions.\n * @param e - Browser event being handled.\n * @returns True when the browser should handle the event, or this router after interception.\n */\n eventPushStateClick(e: MouseEvent) {\n\n if (\n e.defaultPrevented ||\n (e.button !== undefined && e.button !== 0) ||\n e.metaKey || e.ctrlKey || e.shiftKey || e.altKey\n ) {\n return true;\n }\n\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (\n typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement\n );\n const anchor = source && typeof source.closest === 'function'\n ? source.closest('a[href][data-pushstate]')\n : null;\n if (!anchor) {\n return true;\n }\n\n const target = anchor.getAttribute('target');\n if (\n anchor.hasAttribute('download') ||\n (target && target.toLowerCase() !== '_self')\n ) {\n return true;\n }\n\n const location = new URL(\n anchor.getAttribute('href') || '',\n window.location.href || `${window.location.origin}/`\n );\n if (location.origin !== window.location.origin) {\n return true;\n }\n\n e.preventDefault();\n\n // closest() supports nested elements at any depth inside the selected anchor.\n this.url = `${location.pathname}${location.search}${location.hash}`;\n\n this.navigate(false, {}, false);\n\n return this;\n\n }\n\n /**\n * Restore browser history state without adding a new history entry.\n * @param e - Browser event being handled.\n * @returns This router after restoring the history state.\n */\n eventPopState(e: PopStateEvent) {\n\n this.url = (e.state || '');\n\n this.navigate(false, {}, true);\n\n return this;\n\n }\n\n /**\n * Decode URL query parameters, keeping the last value for duplicate keys.\n * @param queryString - Encoded URL query portion.\n * @returns A record of decoded query names and their final values.\n */\n parseQueryString(queryString: string) {\n return Object.fromEntries(new URLSearchParams(queryString));\n\n }\n\n /**\n * Build decoded path, query, and mediator data for the selected route.\n * @param mediatorData - Optional caller metadata passed to the route.\n * @returns No value; locationData is updated in place.\n */\n setLocationData(mediatorData?: NavigationData) {\n\n const parsedUrl = new URL(this.url, window.location.origin);\n const matchedRoute = this.route && this.route !== 'defaultRoute' ? this.route : '';\n\n // this is the object passed to the matching view\n this.locationData = {\n url: this.url,\n data: {\n url: [],\n mediator: mediatorData,\n query: {}\n }\n };\n\n // parse out the url data\n this.locationData.data.url = parsedUrl.pathname\n .slice(matchedRoute.length)\n .split('/')\n .filter(Boolean)\n .map((fragment) => {\n try {\n return decodeURIComponent(fragment);\n } catch (error) {\n return fragment;\n }\n });\n\n //add query string data\n this.locationData.data.query = this.parseQueryString(parsedUrl.search);\n\n }\n\n /**\n * Select a route, enforce its guard, transition view lifecycles, and update history and title.\n * @param url - URL or optional adapter argument.\n * @param mediatorData - Optional caller metadata passed to the route.\n * @param isPopState - Whether navigation came from browser history.\n * @returns False when a guard or route rejects navigation; otherwise this router.\n */\n navigate(url?: string | false, mediatorData?: NavigationData, isPopState = false) {\n\n // allow navigate to use a specified url\n if (url) {\n this.url = url;\n }\n\n //clean up url for matching\n this.url = this.url.replace(window.location.origin, '');\n\n // reset this to null for new location\n this.route = null;\n\n const pathname = new URL(this.url, window.location.origin).pathname;\n\n // Match complete path segments so /page2 does not also match /page23.\n for (let route in this.routes) {\n\n if (\n route !== 'defaultRoute' &&\n (pathname === route || pathname.startsWith(`${route}/`))\n ) {\n\n this.route = route;\n\n //stop looping we are done\n break;\n\n }\n\n }\n\n this.setLocationData(mediatorData);\n\n // if a catch all default route is defined execute that when not match is found\n if (!this.route && this.routes.defaultRoute) {\n\n this.route = 'defaultRoute';\n\n }\n\n if (this.route) {\n const selected = this.routes[this.route];\n if (typeof selected !== 'function' && selected.secure &&\n selected.secure(this.scope, this.locationData) !== true) {\n return false;\n }\n const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;\n if (previous && typeof previous !== 'function' && typeof previous.view === 'object' && previous.view.destroy) {\n previous.view.destroy(this.scope, this.locationData);\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 this.pageTitle = typeof selected.title === 'string' ? selected.title : null;\n } else if (selected.view && typeof selected.view.initialize === 'function') {\n this.pageTitle = typeof selected.title === 'string' ? selected.title : null;\n selected.view.initialize(this.scope, this.locationData);\n } else {\n return false;\n }\n this.previousRoute = this.route;\n }\n\n // make sure to not set pushstate on back button click\n if (!isPopState) {\n //set in history the new url\n window.history.pushState(this.url, this.pageTitle || '', this.url);\n }\n\n // since browsers don't support setting the title with pushstate yet\n if (this.pageTitle) {\n document.title = this.pageTitle;\n }\n\n return this;\n\n }\n\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":";AAaA,oFAAoF;AACpF,MAAM,MAAM;IACR,MAAM,CAAwB;IAC9B,GAAG,CAAS;IACZ,KAAK,CAAgB;IACrB,KAAK,CAAiB;IACtB,aAAa,CAAgB;IAC7B,SAAS,CAAgB;IACzB,QAAQ,CAA6B;IACrC,mBAAmB,CAAiC;IACpD,aAAa,CAAoC;IACjD,qBAAqB,CAAqC;IAC1D,oBAAoB,CAAU;IAC9B,YAAY,GAAiB,EAAC,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAC,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAC,EAAC,CAAC;IAExF,qEAAqE;IACrE;QACI,IAAI,CAAC,MAAM,GAAG;YACV,YAAY,EAAE;gBACV,IAAI,EAAE;oBACF,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI;oBACtB,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI;iBACtB;aACJ;SACJ,CAAC;QACF,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,mBAAmB,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/D,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,qBAAqB,GAAG,CAAC,IAAI,GAAG,EAAE,EAAE,EAAE;YACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;QACzC,CAAC,CAAC;QACF,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;IACtC,CAAC;IAED,uDAAuD;IAC/C,gBAAgB;QACpB,OAAO,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,QAAQ,KAAK,WAAW,CAAC;IAC5E,CAAC;IAED,6EAA6E;IACrE,SAAS;QACb,OAAO,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,4BAA4B,CAAC;IAC3F,CAAC;IAED,6EAA6E;IACrE,aAAa;QACjB,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC;QAAA,CAAC;QACvD,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;IAC5F,CAAC;IAED,2EAA2E;IACnE,mBAAmB,CAAC,GAAW;QACnC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAAA,OAAO,GAAG,CAAC;QAAA,CAAC;QAC3C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QAC5F,IAAI,SAAS,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC/D,OAAO,GAAG,SAAS,CAAC,QAAQ,GAAG,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;IACvE,CAAC;IAED;;;OAGG;IACH,UAAU,CAAC,GAAY;QACnB,IAAI,CAAC,GAAG,GAAG,GAAG,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACvC,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,4DAA4D;IAC5D,YAAY;QACR,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC7C,IAAI,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAC1B,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;YAC7D,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5D,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAAA,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,iBAAiB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAAA,CAAC;QACrF,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,6FAA6F;IAC7F,OAAO;QACH,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,0FAA0F;IAC1F,eAAe;QACX,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC9C,IAAI,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAC1B,QAAQ,CAAC,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;YAChE,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAC/D,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAAA,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,iBAAiB,EAAE,IAAI,CAAC,qBAAqB,CAAC,CAAC;QAAA,CAAC;QACjG,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;QAClC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,+FAA+F;IAC/F,mBAAmB,CAAC,CAAa;QAC7B,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QACjD,IAAI,CAAC,CAAC,gBAAgB,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;YACvH,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,WAAW,GAAG,CAAC,CAAC,MAAwB,CAAC;QAC/C,MAAM,MAAM,GAAG,WAAW,IAAI,CAAC,OAAO,WAAW,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC;QACpH,MAAM,MAAM,GAAG,MAAM,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACjH,IAAI,CAAC,MAAM,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,WAAW,EAAE,KAAK,OAAO,CAAC,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QACnG,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QAClH,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC9D,CAAC,CAAC,cAAc,EAAE,CAAC;QACnB,IAAI,CAAC,GAAG,GAAG,GAAG,QAAQ,CAAC,QAAQ,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;QACpE,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,wEAAwE;IACxE,aAAa;QACT,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QACjD,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QAChC,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,8EAA8E;IAC9E,gBAAgB,CAAC,WAAmB;QAChC,OAAO,MAAM,CAAC,WAAW,CAAC,IAAI,eAAe,CAAC,WAAW,CAAC,CAAC,CAAC;IAChE,CAAC;IAED,2EAA2E;IAC3E,eAAe,CAAC,YAA6B;QACzC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QAC7D,MAAM,YAAY,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QACnF,IAAI,CAAC,YAAY,GAAG;YAChB,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,IAAI,EAAE,EAAC,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,EAAC;SACrD,CAAC;QACF,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,GAAG,SAAS,CAAC,QAAQ;aAC1C,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC;aAC1B,KAAK,CAAC,GAAG,CAAC;aACV,MAAM,CAAC,OAAO,CAAC;aACf,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE;YACd,IAAI,CAAC;gBAAA,OAAO,kBAAkB,CAAC,QAAQ,CAAC,CAAC;YAAA,CAAC;YAAC,OAAO,MAAM,EAAE,CAAC;gBAAA,OAAO,QAAQ,CAAC;YAAA,CAAC;QAChF,CAAC,CAAC,CAAC;QACP,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC3E,CAAC;IAED,+EAA+E;IAC/E,gBAAgB,CAAC,KAAY;QACzB,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;YAC9B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAAA,OAAO,IAAI,CAAC;QAAA,CAAC;QAC5C,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YAAA,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC;QAAA,CAAC;QACtD,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;YACxB,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAc,KAAK,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC;YAC7E,IAAI,MAAM,EAAE,CAAC;gBACT,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,EAAE,CAAC;oBAAA,MAAM,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;gBAAA,CAAC;gBAC9E,MAAM,CAAC,KAAK,EAAE,CAAC;YACnB,CAAC;QACL,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,gHAAgH;IAChH,QAAQ,CAAC,GAAY,EAAE,YAA6B,EAAE,UAAU,GAAG,KAAK;QACpE,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YAAA,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QAAA,CAAC;QACxC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;YAAA,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;QAAA,CAAC;QACjD,MAAM,aAAa,GAAG,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACzD,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;YAAA,OAAO,KAAK,CAAC;QAAA,CAAC;QAC3C,IAAI,CAAC,GAAG,GAAG,aAAa,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,QAAQ,CAAC;QAC9D,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAC9B,IAAI,KAAK,KAAK,cAAc,IAAI,CAAC,QAAQ,KAAK,KAAK,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,EAAE,CAAC;gBAAA,SAAS;YAAA,CAAC;YACtG,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;gBAAA,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;YAAA,CAAC;QAC9E,CAAC;QACD,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAAA,IAAI,CAAC,KAAK,GAAG,cAAc,CAAC;QAAA,CAAC;QAC3E,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAE,CAAC;YAC1C,IAAI,OAAO,QAAQ,KAAK,UAAU,IAAI,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC/G,OAAO,KAAK,CAAC;YACjB,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAClF,IAAI,QAAQ,IAAI,OAAO,QAAQ,KAAK,UAAU,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;gBAC3G,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YACzD,CAAC;YACD,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;gBACjC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YAC5C,CAAC;iBAAM,IAAI,OAAO,QAAQ,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBAC7C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YACjD,CAAC;iBAAM,IAAI,QAAQ,CAAC,IAAI,IAAI,OAAO,QAAQ,CAAC,IAAI,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;gBACzE,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;YAC5D,CAAC;iBAAM,CAAC;gBACJ,OAAO,KAAK,CAAC;YACjB,CAAC;YACD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAChC,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC;QACpC,CAAC;QACD,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,gBAAgB,EAAE,EAAE,CAAC;YACzC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,IAAI,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;CACJ;AASD,iBAAS,MAAM,CAAC","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;\n/** An object route can own a function or a view lifecycle. */\ninterface RouteObject {title?: string; focus?: string | false; secure?: RouteHandler; view?: RouteHandler | {initialize?: RouteHandler; destroy?: RouteHandler}}\ntype Route = RouteHandler | RouteObject;\n/** Minimal event-bus contract required by the router. */\ninterface NavigationMediator {\n on(event: string, callback: (data?: NavigationData) => unknown): unknown;\n removeListener(event: string, callback: (data?: NavigationData) => unknown): unknown;\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 mediator: NavigationMediator | false;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: (data?: NavigationData) => unknown;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\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.mediator = false;\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (data = {}) => {\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\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 /** 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 = new URL(url || '/', window.location.href || `${window.location.origin}/`);\n if (parsedUrl.origin !== window.location.origin) {return null;}\n return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\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.url = url ?? this.getCurrentUrl();\n this.addListeners();\n this.navigate(undefined, {}, 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 document.addEventListener('click', this.boundPushStateClick);\n window.addEventListener('popstate', this.boundPopState);\n }\n if (this.mediator) {this.mediator.on('router:navigate', this.boundMediatorNavigate);}\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 document.removeEventListener('click', this.boundPushStateClick);\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.mediator) {this.mediator.removeListener('router:navigate', this.boundMediatorNavigate);}\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 location = new URL(anchor.getAttribute('href') || '', window.location.href || `${window.location.origin}/`);\n if (location.origin !== window.location.origin) {return true;}\n e.preventDefault();\n this.url = `${location.pathname}${location.search}${location.hash}`;\n this.navigate(undefined, {}, 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.url = this.getCurrentUrl();\n this.navigate(undefined, {}, 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 parsedUrl = new URL(this.url || '/', this.getOrigin());\n const matchedRoute = this.route && this.route !== 'defaultRoute' ? this.route : '';\n this.locationData = {\n url: this.url,\n data: {url: [], mediator: mediatorData, query: {}}\n };\n this.locationData.data.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 this.locationData.data.query = this.parseQueryString(parsedUrl.search);\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) {this.url = url;}\n if (!this.url) {this.url = this.getCurrentUrl();}\n const normalizedUrl = this.normalizeBrowserUrl(this.url);\n if (normalizedUrl === null) {return false;}\n this.url = normalizedUrl;\n this.route = null;\n const pathname = new URL(this.url, this.getOrigin()).pathname;\n for (const route in this.routes) {\n if (route === 'defaultRoute' || (pathname !== route && !pathname.startsWith(`${route}/`))) {continue;}\n if (!this.route || route.length > this.route.length) {this.route = route;}\n }\n this.setLocationData(mediatorData);\n if (!this.route && this.routes.defaultRoute) {this.route = 'defaultRoute';}\n if (this.route) {\n const selected = this.routes[this.route]!;\n if (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, this.locationData) !== true) {\n return false;\n }\n const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;\n if (previous && typeof previous !== 'function' && typeof previous.view === 'object' && previous.view.destroy) {\n previous.view.destroy(this.scope, this.locationData);\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 if (selected.view && typeof selected.view.initialize === 'function') {\n selected.view.initialize(this.scope, this.locationData);\n } else {\n return false;\n }\n this.applyPageContext(selected);\n this.previousRoute = this.route;\n }\n if (!isPopState && this.isBrowserRuntime()) {\n window.history.pushState(this.url, this.pageTitle || '', this.url);\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"]}
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "white-label-router",
3
- "version": "4.0.0",
4
- "description": "A simple ES6 JS router",
3
+ "version": "5.0.0",
4
+ "description": "Framework-independent TypeScript router for browser, server, and request-scoped Node.js functions with progressive enhancement, guards, and accessible navigation.",
5
+ "sideEffects": false,
5
6
  "main": "dist/index.js",
6
7
  "files": [
7
8
  "dist",
8
9
  "README.md"
9
10
  ],
10
11
  "engines": {
11
- "node": "^22.18.0 || >=24.11.0",
12
- "npm": ">=10.0"
12
+ "node": "^22.18.0 || >=24.11.0"
13
13
  },
14
14
  "repository": {
15
15
  "type": "git",
@@ -18,38 +18,53 @@
18
18
  "contributors": [
19
19
  {
20
20
  "name": "Bill Shackelford",
21
- "email": "bshackelford@gmail.com",
22
21
  "url": "https://github.com/bshack"
23
22
  }
24
23
  ],
25
24
  "keywords": [
26
- "js",
27
- "javacript",
25
+ "javascript",
26
+ "typescript",
28
27
  "router",
29
- "es6",
30
- "es2015",
31
- "delegate"
28
+ "routing",
29
+ "client-side-routing",
30
+ "history-api",
31
+ "progressive-enhancement",
32
+ "navigation",
33
+ "accessibility",
34
+ "framework-independent",
35
+ "browser",
36
+ "node",
37
+ "server-side",
38
+ "serverless",
39
+ "universal"
32
40
  ],
33
41
  "author": "bshack",
34
42
  "license": "MIT",
35
43
  "bugs": {
36
44
  "url": "https://github.com/bshack/white-label-router/issues"
37
45
  },
38
- "homepage": "https://github.com/bshack/white-label-router#readme",
46
+ "homepage": "https://whitelabeljs.org/docs/router/",
39
47
  "scripts": {
40
48
  "build": "tsc -p tsconfig.json",
41
- "test": "npm run build && npm run test:types && npm run test:unit",
49
+ "test": "node --run build && node --run test:types && node --run test:unit",
42
50
  "audit": "npm audit --audit-level=high",
43
51
  "typecheck": "tsc -p tsconfig.json --noEmit",
44
52
  "test:unit": "node --test test/*.test.js",
45
- "coverage": "npm run build && npm run test:types && c8 npm run test:unit",
46
- "prepack": "npm run build",
47
- "test:types": "tsc -p tsconfig.consumer.json"
53
+ "coverage": "node --run build && node --run test:types && c8 node --run test:unit",
54
+ "prepare": "node --run build",
55
+ "test:types": "tsc -p tsconfig.consumer.json",
56
+ "lint": "eslint \"src/**/*.ts\" --max-warnings=0",
57
+ "lint:fix": "node --run lint -- --fix"
48
58
  },
49
59
  "devDependencies": {
50
60
  "@types/node": "24.13.3",
51
61
  "c8": "12.0.0",
52
- "typescript": "7.0.2"
62
+ "typescript": "7.0.2",
63
+ "@babel/core": "8.0.5",
64
+ "@babel/eslint-parser": "8.0.5",
65
+ "@babel/preset-typescript": "8.0.1",
66
+ "@eslint/js": "10.0.1",
67
+ "eslint": "10.10.0"
53
68
  },
54
69
  "types": "dist/index.d.ts",
55
70
  "c8": {
@@ -70,8 +85,13 @@
70
85
  "branches": 100,
71
86
  "statements": 100,
72
87
  "perFile": true,
73
- "exclude": [
74
- "**/*.d.ts"
75
- ]
88
+ "exclude": ["**/*.d.ts"]
89
+ },
90
+ "exports": {
91
+ ".": {
92
+ "types": "./dist/index.d.ts",
93
+ "require": "./dist/index.js",
94
+ "default": "./dist/index.js"
95
+ }
76
96
  }
77
97
  }