white-label-router 4.0.0 → 6.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/PACKAGE_MANAGERS.md +19 -0
- package/README.md +169 -126
- package/dist/index.d.ts +36 -52
- package/dist/index.js +148 -133
- package/dist/index.js.map +1 -1
- package/package.json +41 -20
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.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Package manager support
|
|
2
|
+
|
|
3
|
+
`white-label-router` supports npm, Yarn, and pnpm consumers.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install white-label-router
|
|
9
|
+
yarn add white-label-router
|
|
10
|
+
pnpm add white-label-router
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
CI packs the real distributable artifact and installs that same artifact with all three package managers before exercising the public Router API.
|
|
14
|
+
|
|
15
|
+
## Repository development
|
|
16
|
+
|
|
17
|
+
The committed `package-lock.json` remains the repository's canonical dependency lockfile and npm remains the maintenance/audit path used by the primary CI job. Yarn and pnpm compatibility does not require committing `yarn.lock` or `pnpm-lock.yaml`.
|
|
18
|
+
|
|
19
|
+
Nested project scripts use Node's `--run` support so `npm test`, `yarn test`, and `pnpm test` do not depend on another package manager to invoke child scripts.
|
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,156 +138,152 @@ this.routes = {
|
|
|
91
138
|
};
|
|
92
139
|
```
|
|
93
140
|
|
|
94
|
-
|
|
141
|
+
`secure()` must return exactly `true` to allow navigation. Router validates the destination guard and runnable view behavior before tearing down the current route. On a successful route change, the previous route's `destroy()` then 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. Only own route-table properties participate in matching:
|
|
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
|
-
|
|
157
|
+
`/products/sale` selects `saleRoute` even though `/products` was defined first. When no prefix matches, `defaultRoute` runs if configured.
|
|
109
158
|
|
|
110
|
-
##
|
|
159
|
+
## Location contract
|
|
111
160
|
|
|
112
|
-
|
|
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.
|
|
133
|
-
|
|
134
|
-
## Location data
|
|
135
|
-
|
|
136
|
-
Every route receives the configured `scope` and a location object:
|
|
161
|
+
Routes receive the configured `scope` and a location object:
|
|
137
162
|
|
|
138
163
|
```js
|
|
139
164
|
{
|
|
140
165
|
url: '/products/42?color=blue',
|
|
141
166
|
data: {
|
|
142
167
|
url: ['42'],
|
|
143
|
-
mediator: {
|
|
144
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 `CustomEvent.detail`. Values are URI-decoded with standard WHATWG URL APIs; applications must still validate untrusted values.
|
|
175
|
+
|
|
176
|
+
## Programmatic navigation
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
const result = router.navigate('/products/42', {
|
|
180
|
+
source: 'featured-products'
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// `result` is the router on success or false when navigation is rejected.
|
|
184
|
+
```
|
|
157
185
|
|
|
158
|
-
|
|
186
|
+
In browsers, successful navigation updates history unless the call represents `popstate`. On servers it dispatches without History API effects.
|
|
159
187
|
|
|
160
|
-
|
|
188
|
+
Rejected navigation is atomic from Router's perspective: cross-origin browser URLs, failed guards, and routes without runnable view behavior return `false` without destroying the current route, pushing browser history, or replacing the last successful `url`, selected route, or location payload.
|
|
161
189
|
|
|
162
|
-
|
|
190
|
+
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.
|
|
163
191
|
|
|
164
|
-
## Mediator
|
|
192
|
+
## Mediator integration
|
|
165
193
|
|
|
166
|
-
Assign
|
|
194
|
+
Assign any standards-based event target that provides `addEventListener()` and `removeEventListener()`. `white-label-mediator` is the first-party implementation:
|
|
167
195
|
|
|
168
196
|
```js
|
|
169
197
|
import Mediator from 'white-label-mediator';
|
|
170
198
|
|
|
171
199
|
const mediator = new Mediator();
|
|
172
|
-
const router = new
|
|
200
|
+
const router = new Router();
|
|
201
|
+
|
|
173
202
|
router.mediator = mediator;
|
|
174
203
|
router.initialize();
|
|
175
204
|
|
|
176
|
-
mediator.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
205
|
+
mediator.dispatchEvent(new CustomEvent('router:navigate', {
|
|
206
|
+
detail: {
|
|
207
|
+
url: '/account',
|
|
208
|
+
reason: 'Session refreshed'
|
|
209
|
+
}
|
|
210
|
+
}));
|
|
180
211
|
```
|
|
181
212
|
|
|
182
|
-
The
|
|
213
|
+
The `CustomEvent.detail` value becomes `location.data.mediator`. `destroy()` removes the listener owned by Router. Repeated listener initialization is idempotent. Reassigning `router.mediator` while listeners are active removes `router:navigate` from the previous mediator before subscribing to the replacement.
|
|
214
|
+
|
|
215
|
+
Router does not import or require `white-label-mediator`; the integration is structural so another compatible EventTarget can be used instead.
|
|
216
|
+
|
|
217
|
+
## Public API
|
|
183
218
|
|
|
184
|
-
|
|
219
|
+
| Member | Behavior | Returns |
|
|
220
|
+
| --- | --- | --- |
|
|
221
|
+
| `routes` | Route table of functions or lifecycle route objects; matching uses the longest own path prefix, not insertion order. | Configuration property; not a method. |
|
|
222
|
+
| `scope` | Application scope passed to route callbacks. | Configuration property; not a method. |
|
|
223
|
+
| `mediator` | Optional EventTarget-compatible source for `router:navigate`; active subscriptions move when this property changes. | Configuration property; not a method. |
|
|
224
|
+
| `initialize(url?)` | Dispatch the browser URL or an explicit server URL and attach applicable listeners. | The same `Router` instance. |
|
|
225
|
+
| `navigate(url?, data?, isPopState?)` | Match and run a route; update browser history when appropriate. Rejected navigation preserves the current route state. | The same `Router` instance on success; `false` for cross-origin browser URLs, rejected guards, or routes without runnable view behavior. |
|
|
226
|
+
| `addListeners()` | Attach browser and optional mediator listeners once. | The same `Router` instance. |
|
|
227
|
+
| `removeListeners()` | Release listeners owned by this router. | The same `Router` instance. |
|
|
228
|
+
| `destroy()` | Release routing listeners. | The same `Router` instance after cleanup. |
|
|
229
|
+
| `parseQueryString(query)` | Decode a query string using `URLSearchParams`. | A plain object containing the last value for each query key. |
|
|
230
|
+
| `setLocationData(data?)` | Rebuild the current route location payload. | `undefined`; updates `locationData` in place. |
|
|
231
|
+
| `applyPageContext(route)` | Update route title/focus context when applicable. | The same `Router` instance. |
|
|
232
|
+
| `eventPushStateClick(event)` | Handle an eligible browser push-state click. | The same `Router` instance after handled navigation; `true` when native/default handling should continue. |
|
|
233
|
+
| `eventPopState()` | Dispatch the current browser history location. | The same `Router` instance. |
|
|
185
234
|
|
|
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. |
|
|
235
|
+
## Title and focus
|
|
193
236
|
|
|
194
|
-
|
|
237
|
+
Object routes can define browser title and focus behavior:
|
|
195
238
|
|
|
196
239
|
```js
|
|
197
|
-
router.
|
|
240
|
+
router.routes = {
|
|
241
|
+
'/account': {
|
|
242
|
+
title: 'Account',
|
|
243
|
+
focus: '#account-title',
|
|
244
|
+
view: renderAccount
|
|
245
|
+
}
|
|
246
|
+
};
|
|
198
247
|
```
|
|
199
248
|
|
|
200
|
-
|
|
249
|
+
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
250
|
|
|
202
|
-
|
|
203
|
-
npm ci
|
|
204
|
-
npm run build
|
|
205
|
-
npm run typecheck
|
|
206
|
-
npm test
|
|
207
|
-
npm run coverage
|
|
208
|
-
npm run audit
|
|
209
|
-
```
|
|
251
|
+
## Browser history behavior
|
|
210
252
|
|
|
211
|
-
|
|
253
|
+
Initialization dispatches the current URL without adding a duplicate history entry. Back/forward navigation reads `window.location`—including path, query, and hash—even when history state is null or belongs to another application. History state is not treated as the authoritative URL. Rejected navigation does not add a history entry.
|
|
212
254
|
|
|
213
|
-
## TypeScript
|
|
255
|
+
## TypeScript
|
|
214
256
|
|
|
215
|
-
Implementation
|
|
257
|
+
Implementation uses strict TypeScript and emits JavaScript, source maps, and declarations into `dist`.
|
|
216
258
|
|
|
217
259
|
```ts
|
|
218
|
-
import Router from 'white-label-router';
|
|
219
|
-
|
|
220
260
|
const router = new Router();
|
|
221
261
|
const greeting: Router.Route = (scope, location) => {
|
|
222
|
-
console.log(location.data.query.name);
|
|
262
|
+
console.log(location.data.query.name);
|
|
223
263
|
};
|
|
264
|
+
|
|
224
265
|
router.routes['/hello'] = greeting;
|
|
225
266
|
router.initialize();
|
|
226
267
|
```
|
|
227
268
|
|
|
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.
|
|
269
|
+
`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
270
|
|
|
232
|
-
|
|
271
|
+
## Development
|
|
233
272
|
|
|
234
273
|
```sh
|
|
235
274
|
npm ci --ignore-scripts
|
|
275
|
+
npm run lint
|
|
236
276
|
npm run typecheck
|
|
237
277
|
npm test
|
|
238
278
|
npm run coverage
|
|
279
|
+
npm run audit
|
|
239
280
|
npm pack --dry-run
|
|
240
281
|
```
|
|
241
282
|
|
|
242
|
-
|
|
283
|
+
Tests cover browser behavior, DOM-free server routing, atomic rejected navigation, mediator reassignment, and parity of the shared route contract. Coverage enforces 100% statements, branches, functions, and lines per implementation file. CI verifies both the documented Node 22.18 minimum and the primary Node 24 line, builds authored source, audits dependencies, packs the package, and verifies the packed public API across npm, Yarn, and pnpm.
|
|
284
|
+
|
|
285
|
+
Edit `src/*.ts` and regenerate `dist`; do not edit generated files directly.
|
|
243
286
|
|
|
244
|
-
|
|
287
|
+
## Design boundary
|
|
245
288
|
|
|
246
|
-
|
|
289
|
+
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;
|
|
@@ -23,12 +24,12 @@ interface RouteObject {
|
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
type Route = RouteHandler | RouteObject;
|
|
26
|
-
/** Minimal
|
|
27
|
+
/** Minimal standards-based event target contract required by the router. */
|
|
27
28
|
interface NavigationMediator {
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
30
|
+
removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
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;
|
|
@@ -36,70 +37,53 @@ declare class Router {
|
|
|
36
37
|
scope: Element | null;
|
|
37
38
|
previousRoute: string | null;
|
|
38
39
|
pageTitle: string | null;
|
|
39
|
-
mediator: NavigationMediator | false;
|
|
40
40
|
boundPushStateClick: (event: MouseEvent) => unknown;
|
|
41
41
|
boundPopState: (event: PopStateEvent) => unknown;
|
|
42
|
-
boundMediatorNavigate:
|
|
42
|
+
boundMediatorNavigate: EventListener;
|
|
43
43
|
listenersInitialized: boolean;
|
|
44
44
|
locationData: LocationData;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
private currentMediator;
|
|
46
|
+
private boundMediator;
|
|
47
|
+
/** Create an instance with its own state and listener references. */
|
|
48
48
|
constructor();
|
|
49
|
+
get mediator(): NavigationMediator | false;
|
|
50
|
+
/** Move an active router:navigate subscription when the mediator changes. */
|
|
51
|
+
set mediator(value: NavigationMediator | false);
|
|
52
|
+
/** True when browser navigation APIs are available. */
|
|
53
|
+
private isBrowserRuntime;
|
|
54
|
+
/** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
|
|
55
|
+
private getOrigin;
|
|
56
|
+
/** Read the authoritative browser URL, or retain the supplied server URL. */
|
|
57
|
+
private getCurrentUrl;
|
|
58
|
+
/** Normalize browser navigation to a same-origin path, query, and hash. */
|
|
59
|
+
private normalizeBrowserUrl;
|
|
60
|
+
/** Return whether a selected route has runnable view behavior. */
|
|
61
|
+
private isRunnableRoute;
|
|
49
62
|
/**
|
|
50
63
|
* 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.
|
|
64
|
+
* In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
|
|
57
65
|
*/
|
|
66
|
+
initialize(url?: string): this;
|
|
67
|
+
/** Register browser and optional mediator handlers once. */
|
|
58
68
|
addListeners(): this;
|
|
59
|
-
/**
|
|
60
|
-
* Release owned state and listeners so the instance can leave the application lifecycle.
|
|
61
|
-
* @returns This instance after cleanup.
|
|
62
|
-
*/
|
|
69
|
+
/** Release owned state and listeners so the instance can leave the application lifecycle. */
|
|
63
70
|
destroy(): this;
|
|
64
|
-
/**
|
|
65
|
-
* Release listeners owned by this instance; subclasses may extend the lifecycle hook.
|
|
66
|
-
* @returns This instance for chaining.
|
|
67
|
-
*/
|
|
71
|
+
/** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
|
|
68
72
|
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
|
-
*/
|
|
73
|
+
/** Intercept eligible same-origin push-state links while preserving normal browser actions. */
|
|
74
74
|
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
|
-
*/
|
|
75
|
+
/** Restore browser history state without adding a new history entry. */
|
|
76
|
+
eventPopState(): this;
|
|
77
|
+
/** Decode URL query parameters, keeping the last value for duplicate keys. */
|
|
86
78
|
parseQueryString(queryString: string): {
|
|
87
79
|
[k: string]: string;
|
|
88
80
|
};
|
|
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
|
-
*/
|
|
81
|
+
/** Build decoded path, query, and mediator data for the selected route. */
|
|
94
82
|
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;
|
|
83
|
+
/** Update page context after a route renders. DOM effects are browser-only. */
|
|
84
|
+
applyPageContext(route: Route): this;
|
|
85
|
+
/** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
|
|
86
|
+
navigate(url?: string, mediatorData?: NavigationData, isPopState?: boolean): false | this;
|
|
103
87
|
}
|
|
104
88
|
/** Public route configuration and navigation payload types. */
|
|
105
89
|
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;
|
|
@@ -7,129 +7,146 @@ class Router {
|
|
|
7
7
|
scope;
|
|
8
8
|
previousRoute;
|
|
9
9
|
pageTitle;
|
|
10
|
-
mediator;
|
|
11
10
|
boundPushStateClick;
|
|
12
11
|
boundPopState;
|
|
13
12
|
boundMediatorNavigate;
|
|
14
13
|
listenersInitialized;
|
|
15
14
|
locationData = { url: '', data: { url: [], mediator: undefined, query: {} } };
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
currentMediator = false;
|
|
16
|
+
boundMediator = false;
|
|
17
|
+
/** Create an instance with its own state and listener references. */
|
|
19
18
|
constructor() {
|
|
20
|
-
// placeholder for routes defined when extended
|
|
21
19
|
this.routes = {
|
|
22
20
|
defaultRoute: {
|
|
23
21
|
view: {
|
|
24
|
-
initialize: () =>
|
|
25
|
-
|
|
26
|
-
},
|
|
27
|
-
destroy: () => {
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
22
|
+
initialize: () => true,
|
|
23
|
+
destroy: () => true
|
|
30
24
|
}
|
|
31
25
|
}
|
|
32
26
|
};
|
|
33
|
-
// this will hold the current url
|
|
34
27
|
this.url = '';
|
|
35
|
-
// this holds the current route
|
|
36
28
|
this.route = null;
|
|
37
|
-
// can house a container for the router to act upon
|
|
38
29
|
this.scope = null;
|
|
39
|
-
// this holds the previous route for destorying later
|
|
40
30
|
this.previousRoute = null;
|
|
41
|
-
// this will hold the page title (not yet supported by browsers)
|
|
42
31
|
this.pageTitle = null;
|
|
43
|
-
// optional mediator placeholder
|
|
44
|
-
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
|
-
this.boundMediatorNavigate = (
|
|
34
|
+
this.boundMediatorNavigate = (event) => {
|
|
35
|
+
const data = event.detail ?? {};
|
|
49
36
|
this.navigate(data.url, data, false);
|
|
50
37
|
};
|
|
51
38
|
this.listenersInitialized = false;
|
|
52
39
|
}
|
|
40
|
+
get mediator() { return this.currentMediator; }
|
|
41
|
+
/** Move an active router:navigate subscription when the mediator changes. */
|
|
42
|
+
set mediator(value) {
|
|
43
|
+
if (value === this.currentMediator) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (this.listenersInitialized && this.boundMediator) {
|
|
47
|
+
this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);
|
|
48
|
+
this.boundMediator = false;
|
|
49
|
+
}
|
|
50
|
+
this.currentMediator = value;
|
|
51
|
+
if (this.listenersInitialized && value) {
|
|
52
|
+
value.addEventListener('router:navigate', this.boundMediatorNavigate);
|
|
53
|
+
this.boundMediator = value;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** True when browser navigation APIs are available. */
|
|
57
|
+
isBrowserRuntime() {
|
|
58
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
59
|
+
}
|
|
60
|
+
/** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
|
|
61
|
+
getOrigin() {
|
|
62
|
+
return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';
|
|
63
|
+
}
|
|
64
|
+
/** Read the authoritative browser URL, or retain the supplied server URL. */
|
|
65
|
+
getCurrentUrl() {
|
|
66
|
+
if (!this.isBrowserRuntime()) {
|
|
67
|
+
return this.url || '/';
|
|
68
|
+
}
|
|
69
|
+
return window.location.pathname + window.location.search + (window.location.hash || '');
|
|
70
|
+
}
|
|
71
|
+
/** Normalize browser navigation to a same-origin path, query, and hash. */
|
|
72
|
+
normalizeBrowserUrl(url) {
|
|
73
|
+
if (!this.isBrowserRuntime()) {
|
|
74
|
+
return url;
|
|
75
|
+
}
|
|
76
|
+
const parsedUrl = new URL(url || '/', window.location.href || `${window.location.origin}/`);
|
|
77
|
+
if (parsedUrl.origin !== window.location.origin) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
|
|
81
|
+
}
|
|
82
|
+
/** Return whether a selected route has runnable view behavior. */
|
|
83
|
+
isRunnableRoute(route) {
|
|
84
|
+
return typeof route === 'function' || typeof route.view === 'function' ||
|
|
85
|
+
Boolean(route.view && typeof route.view.initialize === 'function');
|
|
86
|
+
}
|
|
53
87
|
/**
|
|
54
88
|
* Start this instance and return it for lifecycle chaining.
|
|
55
|
-
*
|
|
89
|
+
* In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
|
|
56
90
|
*/
|
|
57
|
-
initialize() {
|
|
58
|
-
|
|
59
|
-
this.url = window.location.pathname + window.location.search;
|
|
60
|
-
// set up the events
|
|
91
|
+
initialize(url) {
|
|
92
|
+
this.url = url ?? this.getCurrentUrl();
|
|
61
93
|
this.addListeners();
|
|
62
|
-
|
|
63
|
-
this.navigate(false, {}, false);
|
|
94
|
+
this.navigate(undefined, {}, true);
|
|
64
95
|
return this;
|
|
65
96
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Register stable document, history, and optional mediator handlers once.
|
|
68
|
-
* @returns This instance for chaining.
|
|
69
|
-
*/
|
|
97
|
+
/** Register browser and optional mediator handlers once. */
|
|
70
98
|
addListeners() {
|
|
71
99
|
if (this.listenersInitialized) {
|
|
72
100
|
return this;
|
|
73
101
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.
|
|
102
|
+
if (this.isBrowserRuntime()) {
|
|
103
|
+
document.addEventListener('click', this.boundPushStateClick);
|
|
104
|
+
window.addEventListener('popstate', this.boundPopState);
|
|
105
|
+
}
|
|
106
|
+
if (this.currentMediator) {
|
|
107
|
+
this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);
|
|
108
|
+
this.boundMediator = this.currentMediator;
|
|
81
109
|
}
|
|
82
110
|
this.listenersInitialized = true;
|
|
83
111
|
return this;
|
|
84
112
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Release owned state and listeners so the instance can leave the application lifecycle.
|
|
87
|
-
* @returns This instance after cleanup.
|
|
88
|
-
*/
|
|
113
|
+
/** Release owned state and listeners so the instance can leave the application lifecycle. */
|
|
89
114
|
destroy() {
|
|
90
115
|
this.removeListeners();
|
|
91
116
|
return this;
|
|
92
117
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Release listeners owned by this instance; subclasses may extend the lifecycle hook.
|
|
95
|
-
* @returns This instance for chaining.
|
|
96
|
-
*/
|
|
118
|
+
/** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */
|
|
97
119
|
removeListeners() {
|
|
98
120
|
if (!this.listenersInitialized) {
|
|
99
121
|
return this;
|
|
100
122
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (this.
|
|
106
|
-
this.
|
|
123
|
+
if (this.isBrowserRuntime()) {
|
|
124
|
+
document.removeEventListener('click', this.boundPushStateClick);
|
|
125
|
+
window.removeEventListener('popstate', this.boundPopState);
|
|
126
|
+
}
|
|
127
|
+
if (this.boundMediator) {
|
|
128
|
+
this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);
|
|
129
|
+
this.boundMediator = false;
|
|
107
130
|
}
|
|
108
131
|
this.listenersInitialized = false;
|
|
109
132
|
return this;
|
|
110
133
|
}
|
|
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
|
-
*/
|
|
134
|
+
/** Intercept eligible same-origin push-state links while preserving normal browser actions. */
|
|
116
135
|
eventPushStateClick(e) {
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
if (typeof window === 'undefined') {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
120
140
|
return true;
|
|
121
141
|
}
|
|
122
142
|
const eventTarget = e.target;
|
|
123
143
|
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;
|
|
144
|
+
const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;
|
|
127
145
|
if (!anchor) {
|
|
128
146
|
return true;
|
|
129
147
|
}
|
|
130
148
|
const target = anchor.getAttribute('target');
|
|
131
|
-
if (anchor.hasAttribute('download') ||
|
|
132
|
-
(target && target.toLowerCase() !== '_self')) {
|
|
149
|
+
if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {
|
|
133
150
|
return true;
|
|
134
151
|
}
|
|
135
152
|
const location = new URL(anchor.getAttribute('href') || '', window.location.href || `${window.location.origin}/`);
|
|
@@ -137,47 +154,30 @@ class Router {
|
|
|
137
154
|
return true;
|
|
138
155
|
}
|
|
139
156
|
e.preventDefault();
|
|
140
|
-
|
|
141
|
-
this.url = `${location.pathname}${location.search}${location.hash}`;
|
|
142
|
-
this.navigate(false, {}, false);
|
|
157
|
+
this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false);
|
|
143
158
|
return this;
|
|
144
159
|
}
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.
|
|
152
|
-
this.navigate(false, {}, true);
|
|
160
|
+
/** Restore browser history state without adding a new history entry. */
|
|
161
|
+
eventPopState() {
|
|
162
|
+
if (typeof window === 'undefined') {
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
this.url = this.getCurrentUrl();
|
|
166
|
+
this.navigate(undefined, {}, true);
|
|
153
167
|
return this;
|
|
154
168
|
}
|
|
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
|
-
*/
|
|
169
|
+
/** Decode URL query parameters, keeping the last value for duplicate keys. */
|
|
160
170
|
parseQueryString(queryString) {
|
|
161
171
|
return Object.fromEntries(new URLSearchParams(queryString));
|
|
162
172
|
}
|
|
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
|
-
*/
|
|
173
|
+
/** Build decoded path, query, and mediator data for the selected route. */
|
|
168
174
|
setLocationData(mediatorData) {
|
|
169
|
-
const parsedUrl = new URL(this.url,
|
|
175
|
+
const parsedUrl = new URL(this.url || '/', this.getOrigin());
|
|
170
176
|
const matchedRoute = this.route && this.route !== 'defaultRoute' ? this.route : '';
|
|
171
|
-
// this is the object passed to the matching view
|
|
172
177
|
this.locationData = {
|
|
173
178
|
url: this.url,
|
|
174
|
-
data: {
|
|
175
|
-
url: [],
|
|
176
|
-
mediator: mediatorData,
|
|
177
|
-
query: {}
|
|
178
|
-
}
|
|
179
|
+
data: { url: [], mediator: mediatorData, query: {} }
|
|
179
180
|
};
|
|
180
|
-
// parse out the url data
|
|
181
181
|
this.locationData.data.url = parsedUrl.pathname
|
|
182
182
|
.slice(matchedRoute.length)
|
|
183
183
|
.split('/')
|
|
@@ -186,48 +186,74 @@ class Router {
|
|
|
186
186
|
try {
|
|
187
187
|
return decodeURIComponent(fragment);
|
|
188
188
|
}
|
|
189
|
-
catch (
|
|
189
|
+
catch (_error) {
|
|
190
190
|
return fragment;
|
|
191
191
|
}
|
|
192
192
|
});
|
|
193
|
-
//add query string data
|
|
194
193
|
this.locationData.data.query = this.parseQueryString(parsedUrl.search);
|
|
195
194
|
}
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
/** Update page context after a route renders. DOM effects are browser-only. */
|
|
196
|
+
applyPageContext(route) {
|
|
197
|
+
if (typeof route === 'function') {
|
|
198
|
+
this.pageTitle = null;
|
|
199
|
+
return this;
|
|
200
|
+
}
|
|
201
|
+
this.pageTitle = typeof route.title === 'string' ? route.title : null;
|
|
202
|
+
if (!this.isBrowserRuntime()) {
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
if (this.pageTitle) {
|
|
206
|
+
document.title = this.pageTitle;
|
|
207
|
+
}
|
|
208
|
+
if (route.focus !== false) {
|
|
209
|
+
const target = document.querySelector(route.focus || 'main h1');
|
|
210
|
+
if (target) {
|
|
211
|
+
if (!target.hasAttribute('tabindex')) {
|
|
212
|
+
target.setAttribute('tabindex', '-1');
|
|
213
|
+
}
|
|
214
|
+
target.focus();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
/** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
|
|
203
220
|
navigate(url, mediatorData, isPopState = false) {
|
|
204
|
-
|
|
205
|
-
|
|
221
|
+
const previousUrl = this.url;
|
|
222
|
+
const previousSelectedRoute = this.route;
|
|
223
|
+
const previousLocationData = this.locationData;
|
|
224
|
+
if (url !== undefined) {
|
|
206
225
|
this.url = url;
|
|
207
226
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
227
|
+
if (!this.url) {
|
|
228
|
+
this.url = this.getCurrentUrl();
|
|
229
|
+
}
|
|
230
|
+
const normalizedUrl = this.normalizeBrowserUrl(this.url);
|
|
231
|
+
if (normalizedUrl === null) {
|
|
232
|
+
this.url = previousUrl;
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
this.url = normalizedUrl;
|
|
211
236
|
this.route = null;
|
|
212
|
-
const pathname = new URL(this.url,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
237
|
+
const pathname = new URL(this.url, this.getOrigin()).pathname;
|
|
238
|
+
for (const route of Object.keys(this.routes)) {
|
|
239
|
+
if (route === 'defaultRoute' || (pathname !== route && !pathname.startsWith(`${route}/`))) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!this.route || route.length > this.route.length) {
|
|
217
243
|
this.route = route;
|
|
218
|
-
//stop looping we are done
|
|
219
|
-
break;
|
|
220
244
|
}
|
|
221
245
|
}
|
|
222
246
|
this.setLocationData(mediatorData);
|
|
223
|
-
// if a catch all default route is defined execute that when not match is found
|
|
224
247
|
if (!this.route && this.routes.defaultRoute) {
|
|
225
248
|
this.route = 'defaultRoute';
|
|
226
249
|
}
|
|
227
250
|
if (this.route) {
|
|
228
251
|
const selected = this.routes[this.route];
|
|
229
|
-
if (typeof selected !== 'function' && selected.secure &&
|
|
230
|
-
|
|
252
|
+
if ((typeof selected !== 'function' && selected.secure && selected.secure(this.scope, this.locationData) !== true) ||
|
|
253
|
+
!this.isRunnableRoute(selected)) {
|
|
254
|
+
this.url = previousUrl;
|
|
255
|
+
this.route = previousSelectedRoute;
|
|
256
|
+
this.locationData = previousLocationData;
|
|
231
257
|
return false;
|
|
232
258
|
}
|
|
233
259
|
const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;
|
|
@@ -239,29 +265,18 @@ class Router {
|
|
|
239
265
|
}
|
|
240
266
|
else if (typeof selected.view === 'function') {
|
|
241
267
|
selected.view(this.scope, this.locationData);
|
|
242
|
-
this.pageTitle = typeof selected.title === 'string' ? selected.title : null;
|
|
243
|
-
}
|
|
244
|
-
else if (selected.view && typeof selected.view.initialize === 'function') {
|
|
245
|
-
this.pageTitle = typeof selected.title === 'string' ? selected.title : null;
|
|
246
|
-
selected.view.initialize(this.scope, this.locationData);
|
|
247
268
|
}
|
|
248
269
|
else {
|
|
249
|
-
|
|
270
|
+
selected.view.initialize(this.scope, this.locationData);
|
|
250
271
|
}
|
|
272
|
+
this.applyPageContext(selected);
|
|
251
273
|
this.previousRoute = this.route;
|
|
252
274
|
}
|
|
253
|
-
|
|
254
|
-
if (!isPopState) {
|
|
255
|
-
//set in history the new url
|
|
275
|
+
if (!isPopState && this.isBrowserRuntime()) {
|
|
256
276
|
window.history.pushState(this.url, this.pageTitle || '', this.url);
|
|
257
277
|
}
|
|
258
|
-
// since browsers don't support setting the title with pushstate yet
|
|
259
|
-
if (this.pageTitle) {
|
|
260
|
-
document.title = this.pageTitle;
|
|
261
|
-
}
|
|
262
278
|
return this;
|
|
263
279
|
}
|
|
264
280
|
}
|
|
265
|
-
;
|
|
266
281
|
module.exports = Router;
|
|
267
282
|
//# 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 standards-based event target contract required by the router. */\ninterface NavigationMediator {\n addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;\n removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;\n}\n/** History API router that also dispatches request URLs in non-browser runtimes. */\nclass Router {\n routes: Record<string, Route>;\n url: string;\n route: string | null;\n scope: Element | null;\n previousRoute: string | null;\n pageTitle: string | null;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: EventListener;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n private currentMediator: NavigationMediator | false = false;\n private boundMediator: NavigationMediator | false = false;\n\n /** Create an instance with its own state and listener references. */\n constructor() {\n this.routes = {\n defaultRoute: {\n view: {\n initialize: () => true,\n destroy: () => true\n }\n }\n };\n this.url = '';\n this.route = null;\n this.scope = null;\n this.previousRoute = null;\n this.pageTitle = null;\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (event: Event) => {\n const data = (event as CustomEvent<NavigationData | undefined>).detail ?? {};\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n }\n\n get mediator(): NavigationMediator | false {return this.currentMediator;}\n\n /** Move an active router:navigate subscription when the mediator changes. */\n set mediator(value: NavigationMediator | false) {\n if (value === this.currentMediator) {return;}\n if (this.listenersInitialized && this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.currentMediator = value;\n if (this.listenersInitialized && value) {\n value.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = value;\n }\n }\n\n /** 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 /** Return whether a selected route has runnable view behavior. */\n private isRunnableRoute(route: Route): boolean {\n return typeof route === 'function' || typeof route.view === 'function' ||\n Boolean(route.view && typeof route.view.initialize === 'function');\n }\n\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.currentMediator) {\n this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = this.currentMediator;\n }\n this.listenersInitialized = true;\n return this;\n }\n\n /** Release owned state and listeners so the instance can leave the application lifecycle. */\n destroy() {\n this.removeListeners();\n return this;\n }\n\n /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */\n removeListeners() {\n if (!this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n document.removeEventListener('click', this.boundPushStateClick);\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.listenersInitialized = false;\n return this;\n }\n\n /** Intercept eligible same-origin push-state links while preserving normal browser actions. */\n eventPushStateClick(e: MouseEvent) {\n if (typeof window === 'undefined') {return true;}\n if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return true;\n }\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);\n const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;\n if (!anchor) {return true;}\n const target = anchor.getAttribute('target');\n if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {return true;}\n const 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.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false);\n return this;\n }\n\n /** Restore browser history state without adding a new history entry. */\n eventPopState() {\n if (typeof window === 'undefined') {return this;}\n this.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 const previousUrl = this.url;\n const previousSelectedRoute = this.route;\n const previousLocationData = this.locationData;\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) {\n this.url = previousUrl;\n return false;\n }\n this.url = normalizedUrl;\n this.route = null;\n const pathname = new URL(this.url, this.getOrigin()).pathname;\n for (const route of Object.keys(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 !this.isRunnableRoute(selected)) {\n this.url = previousUrl;\n this.route = previousSelectedRoute;\n this.locationData = previousLocationData;\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 {\n selected.view!.initialize!(this.scope, this.locationData);\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,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "white-label-router",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "6.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
|
-
"README.md"
|
|
9
|
+
"README.md",
|
|
10
|
+
"PACKAGE_MANAGERS.md"
|
|
9
11
|
],
|
|
10
12
|
"engines": {
|
|
11
|
-
"node": "^22.18.0 || >=24.11.0"
|
|
12
|
-
"npm": ">=10.0"
|
|
13
|
+
"node": "^22.18.0 || >=24.11.0"
|
|
13
14
|
},
|
|
14
15
|
"repository": {
|
|
15
16
|
"type": "git",
|
|
@@ -18,38 +19,53 @@
|
|
|
18
19
|
"contributors": [
|
|
19
20
|
{
|
|
20
21
|
"name": "Bill Shackelford",
|
|
21
|
-
"email": "bshackelford@gmail.com",
|
|
22
22
|
"url": "https://github.com/bshack"
|
|
23
23
|
}
|
|
24
24
|
],
|
|
25
25
|
"keywords": [
|
|
26
|
-
"
|
|
27
|
-
"
|
|
26
|
+
"javascript",
|
|
27
|
+
"typescript",
|
|
28
28
|
"router",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
29
|
+
"routing",
|
|
30
|
+
"client-side-routing",
|
|
31
|
+
"history-api",
|
|
32
|
+
"progressive-enhancement",
|
|
33
|
+
"navigation",
|
|
34
|
+
"accessibility",
|
|
35
|
+
"framework-independent",
|
|
36
|
+
"browser",
|
|
37
|
+
"node",
|
|
38
|
+
"server-side",
|
|
39
|
+
"serverless",
|
|
40
|
+
"universal"
|
|
32
41
|
],
|
|
33
42
|
"author": "bshack",
|
|
34
43
|
"license": "MIT",
|
|
35
44
|
"bugs": {
|
|
36
45
|
"url": "https://github.com/bshack/white-label-router/issues"
|
|
37
46
|
},
|
|
38
|
-
"homepage": "https://
|
|
47
|
+
"homepage": "https://whitelabeljs.org/docs/router/",
|
|
39
48
|
"scripts": {
|
|
40
49
|
"build": "tsc -p tsconfig.json",
|
|
41
|
-
"test": "
|
|
50
|
+
"test": "node --run build && node --run test:types && node --run test:unit",
|
|
42
51
|
"audit": "npm audit --audit-level=high",
|
|
43
52
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
44
53
|
"test:unit": "node --test test/*.test.js",
|
|
45
|
-
"coverage": "
|
|
46
|
-
"
|
|
47
|
-
"test:types": "tsc -p tsconfig.consumer.json"
|
|
54
|
+
"coverage": "node --run build && node --run test:types && c8 node --run test:unit",
|
|
55
|
+
"prepare": "node --run build",
|
|
56
|
+
"test:types": "tsc -p tsconfig.consumer.json",
|
|
57
|
+
"lint": "eslint \"src/**/*.ts\" --max-warnings=0",
|
|
58
|
+
"lint:fix": "node --run lint -- --fix"
|
|
48
59
|
},
|
|
49
60
|
"devDependencies": {
|
|
50
61
|
"@types/node": "24.13.3",
|
|
51
62
|
"c8": "12.0.0",
|
|
52
|
-
"typescript": "7.0.2"
|
|
63
|
+
"typescript": "7.0.2",
|
|
64
|
+
"@babel/core": "8.0.5",
|
|
65
|
+
"@babel/eslint-parser": "8.0.5",
|
|
66
|
+
"@babel/preset-typescript": "8.0.1",
|
|
67
|
+
"@eslint/js": "10.0.1",
|
|
68
|
+
"eslint": "10.10.0"
|
|
53
69
|
},
|
|
54
70
|
"types": "dist/index.d.ts",
|
|
55
71
|
"c8": {
|
|
@@ -70,8 +86,13 @@
|
|
|
70
86
|
"branches": 100,
|
|
71
87
|
"statements": 100,
|
|
72
88
|
"perFile": true,
|
|
73
|
-
"exclude": [
|
|
74
|
-
|
|
75
|
-
|
|
89
|
+
"exclude": ["**/*.d.ts"]
|
|
90
|
+
},
|
|
91
|
+
"exports": {
|
|
92
|
+
".": {
|
|
93
|
+
"types": "./dist/index.d.ts",
|
|
94
|
+
"require": "./dist/index.js",
|
|
95
|
+
"default": "./dist/index.js"
|
|
96
|
+
}
|
|
76
97
|
}
|
|
77
98
|
}
|