white-label-router 1.1.2 → 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 +188 -266
- package/dist/index.d.ts +89 -0
- package/dist/index.js +245 -342
- package/dist/index.js.map +1 -0
- package/package.json +70 -16
- package/.babelrc +0 -4
- package/.editorconfig +0 -13
- package/.npmignore +0 -2
- package/.travis.yml +0 -5
- package/bin/compile.sh +0 -2
- package/bower.json +0 -26
- package/src/index.js +0 -325
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,361 +1,283 @@
|
|
|
1
1
|
# white-label-router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Turn URLs into application intent without hiding the web platform.
|
|
4
4
|
|
|
5
|
-
|
|
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
6
|
|
|
7
|
-
https://
|
|
7
|
+
[Documentation](https://whitelabeljs.org/docs/router/) · [API reference](https://whitelabeljs.org/api/#router) · [Demo site](https://whitelabeljs.org/)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Install the node module:
|
|
9
|
+
**Responsibility:** translate navigation into application intent. Nothing more.
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
npm install white-label-router --save
|
|
15
|
-
```
|
|
11
|
+
## Why it exists
|
|
16
12
|
|
|
17
|
-
|
|
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.
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
import Router from 'white-label-router';
|
|
21
|
-
```
|
|
15
|
+
Use it independently or compose it with the rest of White Label:
|
|
22
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.
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
The package has no runtime dependency on the other White Label packages.
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
## Requirements
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.routes = {
|
|
33
|
-
defaultRoute: (scope, locationData) => {
|
|
34
|
-
//here you would put any view specific logic for the defaultRoute
|
|
35
|
-
window.console.log('the defaultRoute executed');
|
|
36
|
-
},
|
|
37
|
-
'/page2': (scope, locationData) => {
|
|
38
|
-
//here you would put any view specific logic for the page2 route
|
|
39
|
-
window.console.log('the page2 route executed');
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
```
|
|
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.
|
|
45
30
|
|
|
46
|
-
|
|
31
|
+
## Install
|
|
47
32
|
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.routes = {
|
|
53
|
-
defaultRoute: {
|
|
54
|
-
view: {
|
|
55
|
-
initialize: (scope, locationData) => {
|
|
56
|
-
window.console.log('the defaultRoute initialized');
|
|
57
|
-
},
|
|
58
|
-
destroy: (scope, locationData) => {
|
|
59
|
-
window.console.log('the defaultRoute has been destroyed');
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
'/page2': {
|
|
64
|
-
view: {
|
|
65
|
-
initialize: (scope, locationData) => {
|
|
66
|
-
window.console.log('the page2 initialized');
|
|
67
|
-
},
|
|
68
|
-
destroy: (scope, locationData) => {
|
|
69
|
-
window.console.log('the page2 has been destroyed');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
};
|
|
33
|
+
```sh
|
|
34
|
+
npm install white-label-router
|
|
35
|
+
# or: yarn add white-label-router
|
|
36
|
+
# or: pnpm add white-label-router
|
|
76
37
|
```
|
|
77
38
|
|
|
78
|
-
|
|
39
|
+
The same entrypoint is used in browsers and servers:
|
|
79
40
|
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
constructor() {
|
|
83
|
-
super();
|
|
84
|
-
this.routes = {
|
|
85
|
-
defaultRoute: {
|
|
86
|
-
title: 'Home',
|
|
87
|
-
view: {
|
|
88
|
-
initialize: (scope, locationData) => {
|
|
89
|
-
window.console.log('the defaultRoute initialized');
|
|
90
|
-
},
|
|
91
|
-
destroy: (scope, locationData) => {
|
|
92
|
-
window.console.log('the defaultRoute has been destroyed');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
'/page2': {
|
|
97
|
-
title: 'Page 2',
|
|
98
|
-
view: {
|
|
99
|
-
initialize: (scope, locationData) => {
|
|
100
|
-
window.console.log('the page2 initialized');
|
|
101
|
-
},
|
|
102
|
-
destroy: (scope, locationData) => {
|
|
103
|
-
window.console.log('the page2 has been destroyed');
|
|
104
|
-
}
|
|
105
|
-
},
|
|
106
|
-
secure: this.isSecure
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
isSecure (scope, locationData) => {
|
|
111
|
-
if (someCookieIsValid) {
|
|
112
|
-
return true;
|
|
113
|
-
} else {
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
};
|
|
41
|
+
```js
|
|
42
|
+
import Router from 'white-label-router';
|
|
118
43
|
```
|
|
119
44
|
|
|
120
|
-
|
|
45
|
+
## One route contract, two runtimes
|
|
121
46
|
|
|
122
|
-
|
|
47
|
+
### Browser
|
|
123
48
|
|
|
124
|
-
|
|
49
|
+
Omit the URL to dispatch the current browser location and attach browser navigation listeners:
|
|
125
50
|
|
|
51
|
+
```js
|
|
52
|
+
const router = new Router();
|
|
53
|
+
router.routes = {
|
|
54
|
+
'/products': (_scope, location) => console.log(location.data.url),
|
|
55
|
+
defaultRoute: () => true
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// initialize() dispatches the current URL and returns the router for chaining.
|
|
59
|
+
router.initialize();
|
|
126
60
|
```
|
|
127
|
-
const MyRoute = class extends Router {
|
|
128
|
-
constructor() {
|
|
129
|
-
super();
|
|
130
61
|
|
|
131
|
-
|
|
132
|
-
this.scope = document.querySelector('body');
|
|
62
|
+
### Server
|
|
133
63
|
|
|
134
|
-
|
|
135
|
-
defaultRoute: (scope, locationData) => {
|
|
136
|
-
//here you would put any view specific logic for the defaultRoute
|
|
137
|
-
window.console.log('the defaultRoute executed');
|
|
138
|
-
},
|
|
139
|
-
'/page2': (scope, locationData) => {
|
|
140
|
-
//here you would put any view specific logic for the page2 route
|
|
141
|
-
window.console.log('the page2 route executed');
|
|
142
|
-
}
|
|
143
|
-
};
|
|
64
|
+
Pass the request URL explicitly. No `window` or `document` is required:
|
|
144
65
|
|
|
145
|
-
|
|
66
|
+
```js
|
|
67
|
+
const router = new Router();
|
|
68
|
+
router.routes = {
|
|
69
|
+
'/products': (_scope, location) => console.log(location.data.url),
|
|
70
|
+
defaultRoute: () => true
|
|
146
71
|
};
|
|
72
|
+
router.initialize('/products/42?color=blue');
|
|
147
73
|
```
|
|
148
74
|
|
|
149
|
-
|
|
75
|
+
Browser-only history, click interception, focus, and document-title effects are skipped on the server. Scope mutable routing state per request when appropriate.
|
|
150
76
|
|
|
151
|
-
|
|
152
|
-
const myRouter = new MyRouter();
|
|
77
|
+
## Serverless and function runtimes
|
|
153
78
|
|
|
154
|
-
|
|
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}`);
|
|
155
86
|
```
|
|
156
87
|
|
|
157
|
-
|
|
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.
|
|
158
89
|
|
|
159
|
-
|
|
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.
|
|
160
91
|
|
|
161
|
-
|
|
162
|
-
<a href="/page2" data-pushstate>take me to page2</a>
|
|
163
|
-
```
|
|
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.
|
|
164
93
|
|
|
165
|
-
##
|
|
94
|
+
## Progressive enhancement first
|
|
166
95
|
|
|
167
|
-
|
|
96
|
+
Public navigation should remain a real link:
|
|
168
97
|
|
|
169
|
-
```
|
|
170
|
-
|
|
98
|
+
```html
|
|
99
|
+
<a href="/products/42" data-pushstate>View product 42</a>
|
|
171
100
|
```
|
|
172
101
|
|
|
173
|
-
|
|
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.
|
|
174
103
|
|
|
175
|
-
|
|
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.
|
|
176
105
|
|
|
177
|
-
|
|
178
|
-
const MyRoute = class extends Router {
|
|
179
|
-
constructor() {
|
|
180
|
-
super();
|
|
106
|
+
## Define routes
|
|
181
107
|
|
|
182
|
-
|
|
183
|
-
this.scope = document.querySelector('body');
|
|
108
|
+
A route can be a function:
|
|
184
109
|
|
|
185
|
-
|
|
110
|
+
```js
|
|
111
|
+
router.routes = {
|
|
112
|
+
'/help': (scope, location) => {
|
|
113
|
+
scope.textContent = `Help page: ${location.url}`;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
```
|
|
186
117
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
118
|
+
Or an object describing route lifecycle:
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
router.routes = {
|
|
122
|
+
'/orders': {
|
|
123
|
+
title: 'Orders',
|
|
124
|
+
focus: '#orders-title',
|
|
125
|
+
secure(scope, location) {
|
|
126
|
+
// Guards must return exactly true to allow navigation.
|
|
127
|
+
return Boolean(window.currentUser);
|
|
128
|
+
},
|
|
129
|
+
view: {
|
|
130
|
+
initialize(scope, location) {
|
|
131
|
+
scope.textContent = 'Order history';
|
|
191
132
|
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
window.console.log('the page2 route executed');
|
|
133
|
+
destroy(scope, nextLocation) {
|
|
134
|
+
scope.replaceChildren();
|
|
195
135
|
}
|
|
196
|
-
}
|
|
197
|
-
|
|
136
|
+
}
|
|
198
137
|
}
|
|
199
138
|
};
|
|
200
139
|
```
|
|
201
140
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
```
|
|
205
|
-
myMediator.emit('router:navigate', {
|
|
206
|
-
url: '/authentication',
|
|
207
|
-
message: 'Session expired, please login again.'
|
|
208
|
-
});
|
|
209
|
-
```
|
|
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()`.
|
|
210
142
|
|
|
211
|
-
##
|
|
143
|
+
## Matching
|
|
212
144
|
|
|
213
|
-
|
|
145
|
+
Routes match complete path boundaries. `/products` matches `/products` and `/products/42`, but not `/products-old`.
|
|
214
146
|
|
|
215
|
-
|
|
216
|
-
const MyRoute = class extends Router {
|
|
217
|
-
constructor() {
|
|
218
|
-
super();
|
|
147
|
+
When multiple route prefixes match, Router selects the **longest matching route**, so specific routes win without depending on object insertion order:
|
|
219
148
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
//here you would put any view specific logic for the defaultRoute
|
|
226
|
-
window.console.log('the defaultRoute executed');
|
|
227
|
-
},
|
|
228
|
-
'/page2': (scope, locationData) => {
|
|
229
|
-
//here you would put any view specific logic for the page2 route
|
|
230
|
-
window.console.log('the page2 route executed');
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
}
|
|
149
|
+
```js
|
|
150
|
+
router.routes = {
|
|
151
|
+
'/products': productsRoute,
|
|
152
|
+
'/products/sale': saleRoute,
|
|
153
|
+
defaultRoute
|
|
235
154
|
};
|
|
236
155
|
```
|
|
237
156
|
|
|
238
|
-
|
|
157
|
+
`/products/sale` selects `saleRoute` even though `/products` was defined first. When no prefix matches, `defaultRoute` runs if configured.
|
|
239
158
|
|
|
240
|
-
|
|
159
|
+
## Location contract
|
|
241
160
|
|
|
242
|
-
|
|
161
|
+
Routes receive the configured `scope` and a location object:
|
|
162
|
+
|
|
163
|
+
```js
|
|
243
164
|
{
|
|
244
|
-
url:
|
|
165
|
+
url: '/products/42?color=blue',
|
|
245
166
|
data: {
|
|
246
|
-
url: [],
|
|
247
|
-
mediator: {},
|
|
248
|
-
query: {}
|
|
167
|
+
url: ['42'],
|
|
168
|
+
mediator: {source: 'featured-products'},
|
|
169
|
+
query: {color: 'blue'}
|
|
249
170
|
}
|
|
250
171
|
}
|
|
251
172
|
```
|
|
252
173
|
|
|
253
|
-
|
|
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.
|
|
254
175
|
|
|
255
|
-
|
|
256
|
-
{
|
|
257
|
-
url: ['/user/fred'],
|
|
258
|
-
data: {
|
|
259
|
-
url: ['fred'],
|
|
260
|
-
mediator: {},
|
|
261
|
-
query: {}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
```
|
|
176
|
+
## Programmatic navigation
|
|
265
177
|
|
|
266
|
-
|
|
178
|
+
```js
|
|
179
|
+
const result = router.navigate('/products/42', {
|
|
180
|
+
source: 'featured-products'
|
|
181
|
+
});
|
|
267
182
|
|
|
268
|
-
|
|
269
|
-
{
|
|
270
|
-
url: ['/user?name=fred'],
|
|
271
|
-
data: {
|
|
272
|
-
url: [],
|
|
273
|
-
mediator: {},
|
|
274
|
-
query: {
|
|
275
|
-
name: 'fred'
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
183
|
+
// `result` is the router on success or false when navigation is rejected.
|
|
279
184
|
```
|
|
280
185
|
|
|
281
|
-
|
|
186
|
+
In browsers, successful navigation updates history unless the call represents `popstate`. On servers it dispatches without History API effects.
|
|
282
187
|
|
|
283
|
-
|
|
284
|
-
myMediator.emit('router:navigate', {
|
|
285
|
-
url: '/authentication',
|
|
286
|
-
message: 'Session expired, please login again.'
|
|
287
|
-
});
|
|
288
|
-
```
|
|
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.
|
|
289
189
|
|
|
290
|
-
|
|
190
|
+
## Mediator integration
|
|
291
191
|
|
|
292
|
-
|
|
293
|
-
{
|
|
294
|
-
url: ['/authentication'],
|
|
295
|
-
data: {
|
|
296
|
-
url: [],
|
|
297
|
-
mediator: {
|
|
298
|
-
url: '/authentication',
|
|
299
|
-
message: 'Session expired, please login again.'
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
```
|
|
192
|
+
Assign any EventEmitter-compatible mediator that provides `on()` and `removeListener()`:
|
|
304
193
|
|
|
305
|
-
|
|
194
|
+
```js
|
|
195
|
+
import Mediator from 'white-label-mediator';
|
|
306
196
|
|
|
307
|
-
|
|
197
|
+
const mediator = new Mediator();
|
|
198
|
+
const router = new Router();
|
|
308
199
|
|
|
200
|
+
router.mediator = mediator;
|
|
201
|
+
router.initialize();
|
|
202
|
+
|
|
203
|
+
mediator.emit('router:navigate', {
|
|
204
|
+
url: '/account',
|
|
205
|
+
reason: 'Session refreshed'
|
|
206
|
+
});
|
|
309
207
|
```
|
|
310
|
-
|
|
208
|
+
|
|
209
|
+
The complete event object becomes `location.data.mediator`. `destroy()` removes listeners owned by Router. Repeated listener initialization is idempotent.
|
|
210
|
+
|
|
211
|
+
## Public API
|
|
212
|
+
|
|
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. |
|
|
228
|
+
|
|
229
|
+
## Title and focus
|
|
230
|
+
|
|
231
|
+
Object routes can define browser title and focus behavior:
|
|
232
|
+
|
|
233
|
+
```js
|
|
234
|
+
router.routes = {
|
|
235
|
+
'/account': {
|
|
236
|
+
title: 'Account',
|
|
237
|
+
focus: '#account-title',
|
|
238
|
+
view: renderAccount
|
|
239
|
+
}
|
|
240
|
+
};
|
|
311
241
|
```
|
|
312
242
|
|
|
313
|
-
|
|
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.
|
|
314
244
|
|
|
315
|
-
|
|
316
|
-
const myRouter = class extends Router {
|
|
317
|
-
// this is the constructor. This executed whenever the view is instantiated.
|
|
318
|
-
constructor() {
|
|
245
|
+
## Browser history behavior
|
|
319
246
|
|
|
320
|
-
|
|
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.
|
|
321
248
|
|
|
322
|
-
|
|
323
|
-
this.scope = document.querySelector('body');
|
|
249
|
+
## TypeScript
|
|
324
250
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
//here you would put any view specific logic for the page2 route
|
|
332
|
-
window.console.log('the page2 route executed');
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
}
|
|
251
|
+
Implementation uses strict TypeScript and emits JavaScript, source maps, and declarations into `dist`.
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const router = new Router();
|
|
255
|
+
const greeting: Router.Route = (scope, location) => {
|
|
256
|
+
console.log(location.data.query.name);
|
|
337
257
|
};
|
|
338
258
|
|
|
259
|
+
router.routes['/hello'] = greeting;
|
|
260
|
+
router.initialize();
|
|
339
261
|
```
|
|
340
262
|
|
|
341
|
-
|
|
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.
|
|
342
264
|
|
|
343
|
-
|
|
344
|
-
const myRouter = new MyRoute();
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
initialize your router
|
|
265
|
+
## Development
|
|
348
266
|
|
|
349
|
-
```
|
|
350
|
-
|
|
267
|
+
```sh
|
|
268
|
+
npm ci --ignore-scripts
|
|
269
|
+
npm run lint
|
|
270
|
+
npm run typecheck
|
|
271
|
+
npm test
|
|
272
|
+
npm run coverage
|
|
273
|
+
npm run audit
|
|
274
|
+
npm pack --dry-run
|
|
351
275
|
```
|
|
352
276
|
|
|
353
|
-
|
|
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.
|
|
354
278
|
|
|
355
|
-
|
|
356
|
-
myRouter.navigate('/page2');
|
|
357
|
-
```
|
|
279
|
+
Edit `src/*.ts` and regenerate `dist`; do not edit generated files directly.
|
|
358
280
|
|
|
359
|
-
|
|
281
|
+
## Design boundary
|
|
360
282
|
|
|
361
|
-
|
|
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
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** Navigation payload, including caller-defined metadata. */
|
|
2
|
+
interface NavigationData {
|
|
3
|
+
url?: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
/** URL-derived values supplied to route handlers. */
|
|
7
|
+
interface LocationData {
|
|
8
|
+
url: string;
|
|
9
|
+
data: {
|
|
10
|
+
url: string[];
|
|
11
|
+
mediator: NavigationData | undefined;
|
|
12
|
+
query: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
type RouteHandler = (scope: Element | null, location: LocationData) => unknown;
|
|
16
|
+
/** An object route can own a function or a view lifecycle. */
|
|
17
|
+
interface RouteObject {
|
|
18
|
+
title?: string;
|
|
19
|
+
focus?: string | false;
|
|
20
|
+
secure?: RouteHandler;
|
|
21
|
+
view?: RouteHandler | {
|
|
22
|
+
initialize?: RouteHandler;
|
|
23
|
+
destroy?: RouteHandler;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
type Route = RouteHandler | RouteObject;
|
|
27
|
+
/** Minimal event-bus contract required by the router. */
|
|
28
|
+
interface NavigationMediator {
|
|
29
|
+
on(event: string, callback: (data?: NavigationData) => unknown): unknown;
|
|
30
|
+
removeListener(event: string, callback: (data?: NavigationData) => unknown): unknown;
|
|
31
|
+
}
|
|
32
|
+
/** History API router that also dispatches request URLs in non-browser runtimes. */
|
|
33
|
+
declare class Router {
|
|
34
|
+
routes: Record<string, Route>;
|
|
35
|
+
url: string;
|
|
36
|
+
route: string | null;
|
|
37
|
+
scope: Element | null;
|
|
38
|
+
previousRoute: string | null;
|
|
39
|
+
pageTitle: string | null;
|
|
40
|
+
mediator: NavigationMediator | false;
|
|
41
|
+
boundPushStateClick: (event: MouseEvent) => unknown;
|
|
42
|
+
boundPopState: (event: PopStateEvent) => unknown;
|
|
43
|
+
boundMediatorNavigate: (data?: NavigationData) => unknown;
|
|
44
|
+
listenersInitialized: boolean;
|
|
45
|
+
locationData: LocationData;
|
|
46
|
+
/** Create an instance with its own state and listener references. */
|
|
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;
|
|
56
|
+
/**
|
|
57
|
+
* Start this instance and return it for lifecycle chaining.
|
|
58
|
+
* In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
|
|
59
|
+
*/
|
|
60
|
+
initialize(url?: string): this;
|
|
61
|
+
/** Register browser and optional mediator handlers once. */
|
|
62
|
+
addListeners(): this;
|
|
63
|
+
/** Release owned state and listeners so the instance can leave the application lifecycle. */
|
|
64
|
+
destroy(): this;
|
|
65
|
+
/** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
|
|
66
|
+
removeListeners(): this;
|
|
67
|
+
/** Intercept eligible same-origin push-state links while preserving normal browser actions. */
|
|
68
|
+
eventPushStateClick(e: MouseEvent): true | this;
|
|
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. */
|
|
72
|
+
parseQueryString(queryString: string): {
|
|
73
|
+
[k: string]: string;
|
|
74
|
+
};
|
|
75
|
+
/** Build decoded path, query, and mediator data for the selected route. */
|
|
76
|
+
setLocationData(mediatorData?: NavigationData): void;
|
|
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;
|
|
81
|
+
}
|
|
82
|
+
/** Public route configuration and navigation payload types. */
|
|
83
|
+
declare namespace Router {
|
|
84
|
+
type Handler = RouteHandler;
|
|
85
|
+
type Route = RouteHandler | RouteObject;
|
|
86
|
+
type Location = LocationData;
|
|
87
|
+
type Navigation = NavigationData;
|
|
88
|
+
}
|
|
89
|
+
export = Router;
|