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 +9 -0
- package/README.md +159 -122
- package/dist/index.d.ts +26 -48
- package/dist/index.js +109 -122
- package/dist/index.js.map +1 -1
- package/package.json +39 -19
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
|
-
|
|
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
|
|
8
|
-
-
|
|
9
|
-
-
|
|
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
|
|
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
|
-
##
|
|
45
|
+
## One route contract, two runtimes
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
import Router from 'white-label-router';
|
|
47
|
+
### Browser
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
constructor() {
|
|
28
|
-
super();
|
|
49
|
+
Omit the URL to dispatch the current browser location and attach browser navigation listeners:
|
|
29
50
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
58
|
+
// initialize() dispatches the current URL and returns the router for chaining.
|
|
56
59
|
router.initialize();
|
|
57
60
|
```
|
|
58
61
|
|
|
59
|
-
|
|
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
|
|
108
|
+
A route can be a function:
|
|
64
109
|
|
|
65
110
|
```js
|
|
66
|
-
|
|
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
|
|
118
|
+
Or an object describing route lifecycle:
|
|
74
119
|
|
|
75
120
|
```js
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
Routes match complete path boundaries. `/products` matches `/products` and `/products/42`, but not `/products-old`.
|
|
97
146
|
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
159
|
+
## Location contract
|
|
135
160
|
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
## Programmatic navigation
|
|
159
177
|
|
|
160
|
-
|
|
178
|
+
```js
|
|
179
|
+
const result = router.navigate('/products/42', {
|
|
180
|
+
source: 'featured-products'
|
|
181
|
+
});
|
|
161
182
|
|
|
162
|
-
|
|
183
|
+
// `result` is the router on success or false when navigation is rejected.
|
|
184
|
+
```
|
|
163
185
|
|
|
164
|
-
|
|
186
|
+
In browsers, successful navigation updates history unless the call represents `popstate`. On servers it dispatches without History API effects.
|
|
165
187
|
|
|
166
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
+
Object routes can define browser title and focus behavior:
|
|
195
232
|
|
|
196
233
|
```js
|
|
197
|
-
router.
|
|
234
|
+
router.routes = {
|
|
235
|
+
'/account': {
|
|
236
|
+
title: 'Account',
|
|
237
|
+
focus: '#account-title',
|
|
238
|
+
view: renderAccount
|
|
239
|
+
}
|
|
240
|
+
};
|
|
198
241
|
```
|
|
199
242
|
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
249
|
+
## TypeScript
|
|
214
250
|
|
|
215
|
-
Implementation
|
|
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);
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
+
## Design boundary
|
|
245
282
|
|
|
246
|
-
|
|
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
|
|
30
|
+
removeListener(event: string, callback: (data?: NavigationData) => unknown): unknown;
|
|
30
31
|
}
|
|
31
|
-
/** History API router
|
|
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
|
-
*
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (this.mediator
|
|
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 (
|
|
118
|
-
|
|
119
|
-
|
|
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(
|
|
134
|
+
this.navigate(undefined, {}, false);
|
|
143
135
|
return this;
|
|
144
136
|
}
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
205
|
-
if (url) {
|
|
198
|
+
if (url !== undefined) {
|
|
206
199
|
this.url = url;
|
|
207
200
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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":"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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; 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
|
-
"description": "
|
|
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
|
-
"
|
|
27
|
-
"
|
|
25
|
+
"javascript",
|
|
26
|
+
"typescript",
|
|
28
27
|
"router",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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://
|
|
46
|
+
"homepage": "https://whitelabeljs.org/docs/router/",
|
|
39
47
|
"scripts": {
|
|
40
48
|
"build": "tsc -p tsconfig.json",
|
|
41
|
-
"test": "
|
|
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": "
|
|
46
|
-
"
|
|
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
|
-
|
|
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
|
}
|