white-label-router 5.0.0 → 6.1.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.
@@ -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
@@ -10,7 +10,7 @@
10
10
 
11
11
  ## Why it exists
12
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.
13
+ White Label keeps URLs, links, history, and server requests visible rather than replacing them with a proprietary navigation model. Router adds an application boundary around those platform primitives while preserving normal browser behavior whenever enhancement does not apply.
14
14
 
15
15
  Use it independently or compose it with the rest of White Label:
16
16
 
@@ -21,6 +21,16 @@ Use it independently or compose it with the rest of White Label:
21
21
 
22
22
  The package has no runtime dependency on the other White Label packages.
23
23
 
24
+ ## Where it fits
25
+
26
+ Router is especially useful when URL-backed behavior should be added **without turning the whole page into a client-side application**. Typical fits include progressively enhanced public sites, server-rendered applications, commerce/CMS frontends, account areas, search/filter experiences, and existing applications being modernized one region at a time.
27
+
28
+ For incremental adoption, `navigationRoot` can limit link interception to one owned DOM region while the rest of the page keeps its existing navigation behavior. This makes it possible for multiple independently owned enhancements to coexist without requiring one document-wide router.
29
+
30
+ Router is not a replacement for server routes, authentication/authorization, CDN or API-gateway routing, or a backend framework. Highly client-owned applications can still use it, but its strongest advantage is preserving real links and server-requestable URLs while adding client behavior selectively.
31
+
32
+ See the [incremental server-rendered application guide](https://whitelabeljs.org/guides/incremental-javascript-for-server-rendered-apps/) for the broader integration pattern.
33
+
24
34
  ## Requirements
25
35
 
26
36
  - Node.js `^22.18.0` or `>=24.11.0` for installation, development, and server execution.
@@ -36,8 +46,6 @@ npm install white-label-router
36
46
  # or: pnpm add white-label-router
37
47
  ```
38
48
 
39
- The same entrypoint is used in browsers and servers:
40
-
41
49
  ```js
42
50
  import Router from 'white-label-router';
43
51
  ```
@@ -54,8 +62,6 @@ router.routes = {
54
62
  '/products': (_scope, location) => console.log(location.data.url),
55
63
  defaultRoute: () => true
56
64
  };
57
-
58
- // initialize() dispatches the current URL and returns the router for chaining.
59
65
  router.initialize();
60
66
  ```
61
67
 
@@ -74,23 +80,6 @@ router.initialize('/products/42?color=blue');
74
80
 
75
81
  Browser-only history, click interception, focus, and document-title effects are skipped on the server. Scope mutable routing state per request when appropriate.
76
82
 
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
83
  ## Progressive enhancement first
95
84
 
96
85
  Public navigation should remain a real link:
@@ -103,6 +92,25 @@ Public navigation should remain a real link:
103
92
 
104
93
  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.
105
94
 
95
+ ### Scope enhancement to one existing region
96
+
97
+ By default, Router listens for opted-in links across `document`. Existing server-rendered/CMS/commerce applications can constrain that ownership:
98
+
99
+ ```js
100
+ const router = new Router();
101
+ router.navigationRoot = document.querySelector('[data-product-refinements]');
102
+ router.routes = {
103
+ '/search': (_scope, location) => {
104
+ applyFilters(location.data.query);
105
+ }
106
+ };
107
+ router.initialize();
108
+ ```
109
+
110
+ Only click events that reach `navigationRoot` are considered by that Router. Links elsewhere on the page keep the host application's behavior. Reassigning `navigationRoot` while listeners are active moves the owned click listener to the new root. Setting it to `null` restores document-wide interception.
111
+
112
+ `navigationRoot` is deliberately separate from `scope`: `navigationRoot` controls where enhanced-link clicks are observed; `scope` is application data passed to route callbacks.
113
+
106
114
  ## Define routes
107
115
 
108
116
  A route can be a function:
@@ -123,7 +131,6 @@ router.routes = {
123
131
  title: 'Orders',
124
132
  focus: '#orders-title',
125
133
  secure(scope, location) {
126
- // Guards must return exactly true to allow navigation.
127
134
  return Boolean(window.currentUser);
128
135
  },
129
136
  view: {
@@ -138,13 +145,13 @@ router.routes = {
138
145
  };
139
146
  ```
140
147
 
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()`.
148
+ `secure()` must return exactly `true` to allow navigation. Client guards are interface behavior, not server authorization. Router validates the destination guard and runnable view behavior before tearing down the current route. On a successful route change, the previous route's `destroy()` runs before the next route's `initialize()`.
142
149
 
143
150
  ## Matching
144
151
 
145
152
  Routes match complete path boundaries. `/products` matches `/products` and `/products/42`, but not `/products-old`.
146
153
 
147
- When multiple route prefixes match, Router selects the **longest matching route**, so specific routes win without depending on object insertion order:
154
+ 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:
148
155
 
149
156
  ```js
150
157
  router.routes = {
@@ -171,7 +178,7 @@ Routes receive the configured `scope` and a location object:
171
178
  }
172
179
  ```
173
180
 
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.
181
+ `data.url` contains path segments after the matched prefix, `data.query` contains parsed query values, and `data.mediator` contains navigation data supplied programmatically or through `CustomEvent.detail`. Values are URI-decoded with standard WHATWG URL APIs; applications must still validate untrusted values.
175
182
 
176
183
  ## Programmatic navigation
177
184
 
@@ -179,52 +186,54 @@ Routes receive the configured `scope` and a location object:
179
186
  const result = router.navigate('/products/42', {
180
187
  source: 'featured-products'
181
188
  });
182
-
183
- // `result` is the router on success or false when navigation is rejected.
184
189
  ```
185
190
 
186
191
  In browsers, successful navigation updates history unless the call represents `popstate`. On servers it dispatches without History API effects.
187
192
 
193
+ 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.
194
+
188
195
  Calling `navigate()` without a URL dispatches the current browser URL. In a non-browser runtime the safe default is `/`; server applications should normally supply the request URL explicitly.
189
196
 
190
197
  ## Mediator integration
191
198
 
192
- Assign any EventEmitter-compatible mediator that provides `on()` and `removeListener()`:
199
+ Assign any standards-based event target that provides `addEventListener()` and `removeEventListener()`. `white-label-mediator` is the first-party implementation:
193
200
 
194
201
  ```js
195
202
  import Mediator from 'white-label-mediator';
196
203
 
197
204
  const mediator = new Mediator();
198
205
  const router = new Router();
199
-
200
206
  router.mediator = mediator;
201
207
  router.initialize();
202
208
 
203
- mediator.emit('router:navigate', {
204
- url: '/account',
205
- reason: 'Session refreshed'
206
- });
209
+ mediator.dispatchEvent(new CustomEvent('router:navigate', {
210
+ detail: {
211
+ url: '/account',
212
+ reason: 'Session refreshed'
213
+ }
214
+ }));
207
215
  ```
208
216
 
209
- The complete event object becomes `location.data.mediator`. `destroy()` removes listeners owned by Router. Repeated listener initialization is idempotent.
217
+ The `CustomEvent.detail` value becomes `location.data.mediator`. `destroy()` removes the listener owned by Router. Repeated listener initialization is idempotent. Reassigning `router.mediator` while listeners are active moves the `router:navigate` subscription to the replacement.
210
218
 
211
- ## Public API
219
+ Router does not import or require `white-label-mediator`; the integration is structural so another compatible EventTarget can be used instead.
212
220
 
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. |
221
+ ## Serverless and function runtimes
222
+
223
+ 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`.
224
+
225
+ ```js
226
+ const requestUrl = new URL(request.url);
227
+ const router = new Router();
228
+ router.routes = routes;
229
+ router.initialize(`${requestUrl.pathname}${requestUrl.search}`);
230
+ ```
231
+
232
+ 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.
233
+
234
+ 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.
235
+
236
+ 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.
228
237
 
229
238
  ## Title and focus
230
239
 
@@ -244,7 +253,26 @@ The default focus selector is `main h1`. Set `focus: false` for an in-page refin
244
253
 
245
254
  ## Browser history behavior
246
255
 
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.
256
+ 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.
257
+
258
+ ## Public API
259
+
260
+ | Member | Behavior | Returns |
261
+ | --- | --- | --- |
262
+ | `routes` | Route table of functions or lifecycle route objects; matching uses the longest own path prefix, not insertion order. | Configuration property. |
263
+ | `scope` | Application scope passed to route callbacks. | Configuration property. |
264
+ | `navigationRoot` | Optional browser `Document`/`Element` that owns `data-pushstate` click interception; `null` uses `document`. Active listeners move when reassigned. | Configuration property. |
265
+ | `mediator` | Optional EventTarget-compatible source for `router:navigate`; active subscriptions move when this property changes. | Configuration property. |
266
+ | `initialize(url?)` | Dispatch the browser URL or an explicit server URL and attach applicable listeners. | The same `Router` instance. |
267
+ | `navigate(url?, data?, isPopState?)` | Match and run a route; update browser history when appropriate. Rejected navigation preserves current route state. | The same Router on success; `false` for rejected navigation. |
268
+ | `addListeners()` | Attach browser and optional mediator listeners once. | The same `Router` instance. |
269
+ | `removeListeners()` | Release listeners owned by this router. | The same `Router` instance. |
270
+ | `destroy()` | Release routing listeners. | The same `Router` instance after cleanup. |
271
+ | `parseQueryString(query)` | Decode a query string using `URLSearchParams`. | A plain object containing the last value for each query key. |
272
+ | `setLocationData(data?)` | Rebuild the current route location payload. | `undefined`; updates `locationData` in place. |
273
+ | `applyPageContext(route)` | Update route title/focus context when applicable. | The same `Router` instance. |
274
+ | `eventPushStateClick(event)` | Handle an eligible browser push-state click. | The same Router after handled navigation; `true` when native/default handling should continue. |
275
+ | `eventPopState()` | Dispatch the current browser history location. | The same `Router` instance. |
248
276
 
249
277
  ## TypeScript
250
278
 
@@ -252,10 +280,10 @@ Implementation uses strict TypeScript and emits JavaScript, source maps, and dec
252
280
 
253
281
  ```ts
254
282
  const router = new Router();
283
+ router.navigationRoot = document.querySelector('main');
255
284
  const greeting: Router.Route = (scope, location) => {
256
285
  console.log(location.data.query.name);
257
286
  };
258
-
259
287
  router.routes['/hello'] = greeting;
260
288
  router.initialize();
261
289
  ```
@@ -274,7 +302,7 @@ npm run audit
274
302
  npm pack --dry-run
275
303
  ```
276
304
 
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.
305
+ Tests cover browser behavior, scoped progressive navigation, DOM-free server routing, atomic rejected navigation, mediator reassignment, and parity of the shared route contract. Coverage enforces 100% statements, branches, functions, and lines per implementation file. CI verifies the documented Node minimum and primary Node line, audits dependencies, packs the package, and verifies the public API across npm, Yarn, and pnpm.
278
306
 
279
307
  Edit `src/*.ts` and regenerate `dist`; do not edit generated files directly.
280
308
 
package/dist/index.d.ts CHANGED
@@ -24,10 +24,10 @@ interface RouteObject {
24
24
  };
25
25
  }
26
26
  type Route = RouteHandler | RouteObject;
27
- /** Minimal event-bus contract required by the router. */
27
+ /** Minimal standards-based event target contract required by the router. */
28
28
  interface NavigationMediator {
29
- on(event: string, callback: (data?: NavigationData) => unknown): unknown;
30
- removeListener(event: string, callback: (data?: NavigationData) => unknown): unknown;
29
+ addEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
30
+ removeEventListener(type: string, callback: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
31
31
  }
32
32
  /** History API router that also dispatches request URLs in non-browser runtimes. */
33
33
  declare class Router {
@@ -37,14 +37,23 @@ declare class Router {
37
37
  scope: Element | null;
38
38
  previousRoute: string | null;
39
39
  pageTitle: string | null;
40
- mediator: NavigationMediator | false;
41
40
  boundPushStateClick: (event: MouseEvent) => unknown;
42
41
  boundPopState: (event: PopStateEvent) => unknown;
43
- boundMediatorNavigate: (data?: NavigationData) => unknown;
42
+ boundMediatorNavigate: EventListener;
44
43
  listenersInitialized: boolean;
45
44
  locationData: LocationData;
45
+ private currentMediator;
46
+ private boundMediator;
47
+ private currentNavigationRoot;
48
+ private boundNavigationRoot;
46
49
  /** Create an instance with its own state and listener references. */
47
50
  constructor();
51
+ get mediator(): NavigationMediator | false;
52
+ /** Move an active router:navigate subscription when the mediator changes. */
53
+ set mediator(value: NavigationMediator | false);
54
+ get navigationRoot(): Document | Element | null;
55
+ /** Scope progressive link interception and move an active click listener when the root changes. */
56
+ set navigationRoot(value: Document | Element | null);
48
57
  /** True when browser navigation APIs are available. */
49
58
  private isBrowserRuntime;
50
59
  /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */
@@ -53,6 +62,8 @@ declare class Router {
53
62
  private getCurrentUrl;
54
63
  /** Normalize browser navigation to a same-origin path, query, and hash. */
55
64
  private normalizeBrowserUrl;
65
+ /** Return whether a selected route has runnable view behavior. */
66
+ private isRunnableRoute;
56
67
  /**
57
68
  * Start this instance and return it for lifecycle chaining.
58
69
  * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
package/dist/index.js CHANGED
@@ -7,12 +7,15 @@ 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: {} } };
15
+ currentMediator = false;
16
+ boundMediator = false;
17
+ currentNavigationRoot = null;
18
+ boundNavigationRoot = null;
16
19
  /** Create an instance with its own state and listener references. */
17
20
  constructor() {
18
21
  this.routes = {
@@ -28,14 +31,46 @@ class Router {
28
31
  this.scope = null;
29
32
  this.previousRoute = null;
30
33
  this.pageTitle = null;
31
- this.mediator = false;
32
34
  this.boundPushStateClick = this.eventPushStateClick.bind(this);
33
35
  this.boundPopState = this.eventPopState.bind(this);
34
- this.boundMediatorNavigate = (data = {}) => {
36
+ this.boundMediatorNavigate = (event) => {
37
+ const data = event.detail ?? {};
35
38
  this.navigate(data.url, data, false);
36
39
  };
37
40
  this.listenersInitialized = false;
38
41
  }
42
+ get mediator() { return this.currentMediator; }
43
+ /** Move an active router:navigate subscription when the mediator changes. */
44
+ set mediator(value) {
45
+ if (value === this.currentMediator) {
46
+ return;
47
+ }
48
+ if (this.listenersInitialized && this.boundMediator) {
49
+ this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);
50
+ this.boundMediator = false;
51
+ }
52
+ this.currentMediator = value;
53
+ if (this.listenersInitialized && value) {
54
+ value.addEventListener('router:navigate', this.boundMediatorNavigate);
55
+ this.boundMediator = value;
56
+ }
57
+ }
58
+ get navigationRoot() { return this.currentNavigationRoot; }
59
+ /** Scope progressive link interception and move an active click listener when the root changes. */
60
+ set navigationRoot(value) {
61
+ if (value === this.currentNavigationRoot) {
62
+ return;
63
+ }
64
+ if (this.listenersInitialized && this.isBrowserRuntime()) {
65
+ this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick);
66
+ this.currentNavigationRoot = value;
67
+ const nextRoot = value || document;
68
+ nextRoot.addEventListener('click', this.boundPushStateClick);
69
+ this.boundNavigationRoot = nextRoot;
70
+ return;
71
+ }
72
+ this.currentNavigationRoot = value;
73
+ }
39
74
  /** True when browser navigation APIs are available. */
40
75
  isBrowserRuntime() {
41
76
  return typeof window !== 'undefined' && typeof document !== 'undefined';
@@ -62,6 +97,11 @@ class Router {
62
97
  }
63
98
  return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
64
99
  }
100
+ /** Return whether a selected route has runnable view behavior. */
101
+ isRunnableRoute(route) {
102
+ return typeof route === 'function' || typeof route.view === 'function' ||
103
+ Boolean(route.view && typeof route.view.initialize === 'function');
104
+ }
65
105
  /**
66
106
  * Start this instance and return it for lifecycle chaining.
67
107
  * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.
@@ -78,11 +118,14 @@ class Router {
78
118
  return this;
79
119
  }
80
120
  if (this.isBrowserRuntime()) {
81
- document.addEventListener('click', this.boundPushStateClick);
121
+ const navigationRoot = this.currentNavigationRoot || document;
122
+ navigationRoot.addEventListener('click', this.boundPushStateClick);
123
+ this.boundNavigationRoot = navigationRoot;
82
124
  window.addEventListener('popstate', this.boundPopState);
83
125
  }
84
- if (this.mediator) {
85
- this.mediator.on('router:navigate', this.boundMediatorNavigate);
126
+ if (this.currentMediator) {
127
+ this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);
128
+ this.boundMediator = this.currentMediator;
86
129
  }
87
130
  this.listenersInitialized = true;
88
131
  return this;
@@ -98,11 +141,13 @@ class Router {
98
141
  return this;
99
142
  }
100
143
  if (this.isBrowserRuntime()) {
101
- document.removeEventListener('click', this.boundPushStateClick);
144
+ this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick);
145
+ this.boundNavigationRoot = null;
102
146
  window.removeEventListener('popstate', this.boundPopState);
103
147
  }
104
- if (this.mediator) {
105
- this.mediator.removeListener('router:navigate', this.boundMediatorNavigate);
148
+ if (this.boundMediator) {
149
+ this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);
150
+ this.boundMediator = false;
106
151
  }
107
152
  this.listenersInitialized = false;
108
153
  return this;
@@ -130,8 +175,7 @@ class Router {
130
175
  return true;
131
176
  }
132
177
  e.preventDefault();
133
- this.url = `${location.pathname}${location.search}${location.hash}`;
134
- this.navigate(undefined, {}, false);
178
+ this.navigate(`${location.pathname}${location.search}${location.hash}`, {}, false);
135
179
  return this;
136
180
  }
137
181
  /** Restore browser history state without adding a new history entry. */
@@ -195,6 +239,9 @@ class Router {
195
239
  }
196
240
  /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */
197
241
  navigate(url, mediatorData, isPopState = false) {
242
+ const previousUrl = this.url;
243
+ const previousSelectedRoute = this.route;
244
+ const previousLocationData = this.locationData;
198
245
  if (url !== undefined) {
199
246
  this.url = url;
200
247
  }
@@ -203,12 +250,13 @@ class Router {
203
250
  }
204
251
  const normalizedUrl = this.normalizeBrowserUrl(this.url);
205
252
  if (normalizedUrl === null) {
253
+ this.url = previousUrl;
206
254
  return false;
207
255
  }
208
256
  this.url = normalizedUrl;
209
257
  this.route = null;
210
258
  const pathname = new URL(this.url, this.getOrigin()).pathname;
211
- for (const route in this.routes) {
259
+ for (const route of Object.keys(this.routes)) {
212
260
  if (route === 'defaultRoute' || (pathname !== route && !pathname.startsWith(`${route}/`))) {
213
261
  continue;
214
262
  }
@@ -222,7 +270,11 @@ class Router {
222
270
  }
223
271
  if (this.route) {
224
272
  const selected = this.routes[this.route];
225
- if (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, this.locationData) !== true) {
273
+ if ((typeof selected !== 'function' && selected.secure && selected.secure(this.scope, this.locationData) !== true) ||
274
+ !this.isRunnableRoute(selected)) {
275
+ this.url = previousUrl;
276
+ this.route = previousSelectedRoute;
277
+ this.locationData = previousLocationData;
226
278
  return false;
227
279
  }
228
280
  const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;
@@ -235,11 +287,8 @@ class Router {
235
287
  else if (typeof selected.view === 'function') {
236
288
  selected.view(this.scope, this.locationData);
237
289
  }
238
- else if (selected.view && typeof selected.view.initialize === 'function') {
239
- selected.view.initialize(this.scope, this.locationData);
240
- }
241
290
  else {
242
- return false;
291
+ selected.view.initialize(this.scope, this.locationData);
243
292
  }
244
293
  this.applyPageContext(selected);
245
294
  this.previousRoute = this.route;
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; focus?: string | false; secure?: RouteHandler; view?: RouteHandler | {initialize?: RouteHandler; destroy?: RouteHandler}}\ntype Route = RouteHandler | RouteObject;\n/** Minimal event-bus contract required by the router. */\ninterface NavigationMediator {\n on(event: string, callback: (data?: NavigationData) => unknown): unknown;\n removeListener(event: string, callback: (data?: NavigationData) => unknown): unknown;\n}\n/** History API router that also dispatches request URLs in non-browser runtimes. */\nclass Router {\n routes: Record<string, Route>;\n url: string;\n route: string | null;\n scope: Element | null;\n previousRoute: string | null;\n pageTitle: string | null;\n mediator: NavigationMediator | false;\n boundPushStateClick: (event: MouseEvent) => unknown;\n boundPopState: (event: PopStateEvent) => unknown;\n boundMediatorNavigate: (data?: NavigationData) => unknown;\n listenersInitialized: boolean;\n locationData: LocationData = {url: '', data: {url: [], mediator: undefined, query: {}}};\n\n /** Create an instance with its own state and listener references. */\n constructor() {\n this.routes = {\n defaultRoute: {\n view: {\n initialize: () => true,\n destroy: () => true\n }\n }\n };\n this.url = '';\n this.route = null;\n this.scope = null;\n this.previousRoute = null;\n this.pageTitle = null;\n this.mediator = false;\n this.boundPushStateClick = this.eventPushStateClick.bind(this);\n this.boundPopState = this.eventPopState.bind(this);\n this.boundMediatorNavigate = (data = {}) => {\n this.navigate(data.url, data, false);\n };\n this.listenersInitialized = false;\n }\n\n /** True when browser navigation APIs are available. */\n private isBrowserRuntime() {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n }\n\n /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */\n private getOrigin() {\n return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';\n }\n\n /** Read the authoritative browser URL, or retain the supplied server URL. */\n private getCurrentUrl() {\n if (!this.isBrowserRuntime()) {return this.url || '/';}\n return window.location.pathname + window.location.search + (window.location.hash || '');\n }\n\n /** Normalize browser navigation to a same-origin path, query, and hash. */\n private normalizeBrowserUrl(url: string) {\n if (!this.isBrowserRuntime()) {return url;}\n const parsedUrl = new URL(url || '/', window.location.href || `${window.location.origin}/`);\n if (parsedUrl.origin !== window.location.origin) {return null;}\n return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;\n }\n\n /**\n * Start this instance and return it for lifecycle chaining.\n * In a browser the URL is optional and defaults to window.location. On a server pass the request URL.\n */\n initialize(url?: string) {\n this.url = url ?? this.getCurrentUrl();\n this.addListeners();\n this.navigate(undefined, {}, true);\n return this;\n }\n\n /** Register browser and optional mediator handlers once. */\n addListeners() {\n if (this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n document.addEventListener('click', this.boundPushStateClick);\n window.addEventListener('popstate', this.boundPopState);\n }\n if (this.mediator) {this.mediator.on('router:navigate', this.boundMediatorNavigate);}\n this.listenersInitialized = true;\n return this;\n }\n\n /** Release owned state and listeners so the instance can leave the application lifecycle. */\n destroy() {\n this.removeListeners();\n return this;\n }\n\n /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */\n removeListeners() {\n if (!this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n document.removeEventListener('click', this.boundPushStateClick);\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.mediator) {this.mediator.removeListener('router:navigate', this.boundMediatorNavigate);}\n this.listenersInitialized = false;\n return this;\n }\n\n /** Intercept eligible same-origin push-state links while preserving normal browser actions. */\n eventPushStateClick(e: MouseEvent) {\n if (typeof window === 'undefined') {return true;}\n if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return true;\n }\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);\n const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;\n if (!anchor) {return true;}\n const target = anchor.getAttribute('target');\n if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {return true;}\n const location = new URL(anchor.getAttribute('href') || '', window.location.href || `${window.location.origin}/`);\n if (location.origin !== window.location.origin) {return true;}\n e.preventDefault();\n this.url = `${location.pathname}${location.search}${location.hash}`;\n this.navigate(undefined, {}, false);\n return this;\n }\n\n /** Restore browser history state without adding a new history entry. */\n eventPopState() {\n if (typeof window === 'undefined') {return this;}\n this.url = this.getCurrentUrl();\n this.navigate(undefined, {}, true);\n return this;\n }\n\n /** Decode URL query parameters, keeping the last value for duplicate keys. */\n parseQueryString(queryString: string) {\n return Object.fromEntries(new URLSearchParams(queryString));\n }\n\n /** Build decoded path, query, and mediator data for the selected route. */\n setLocationData(mediatorData?: NavigationData) {\n const parsedUrl = new URL(this.url || '/', this.getOrigin());\n const matchedRoute = this.route && this.route !== 'defaultRoute' ? this.route : '';\n this.locationData = {\n url: this.url,\n data: {url: [], mediator: mediatorData, query: {}}\n };\n this.locationData.data.url = parsedUrl.pathname\n .slice(matchedRoute.length)\n .split('/')\n .filter(Boolean)\n .map((fragment) => {\n try {return decodeURIComponent(fragment);} catch (_error) {return fragment;}\n });\n this.locationData.data.query = this.parseQueryString(parsedUrl.search);\n }\n\n /** Update page context after a route renders. DOM effects are browser-only. */\n applyPageContext(route: Route) {\n if (typeof route === 'function') {\n this.pageTitle = null;\n return this;\n }\n this.pageTitle = typeof route.title === 'string' ? route.title : null;\n if (!this.isBrowserRuntime()) {return this;}\n if (this.pageTitle) {document.title = this.pageTitle;}\n if (route.focus !== false) {\n const target = document.querySelector<HTMLElement>(route.focus || 'main h1');\n if (target) {\n if (!target.hasAttribute('tabindex')) {target.setAttribute('tabindex', '-1');}\n target.focus();\n }\n }\n return this;\n }\n\n /** Select a route, enforce its guard, transition view lifecycles, and update browser history when available. */\n navigate(url?: string, mediatorData?: NavigationData, isPopState = false) {\n if (url !== undefined) {this.url = url;}\n if (!this.url) {this.url = this.getCurrentUrl();}\n const normalizedUrl = this.normalizeBrowserUrl(this.url);\n if (normalizedUrl === null) {return false;}\n this.url = normalizedUrl;\n this.route = null;\n const pathname = new URL(this.url, this.getOrigin()).pathname;\n for (const route in this.routes) {\n if (route === 'defaultRoute' || (pathname !== route && !pathname.startsWith(`${route}/`))) {continue;}\n if (!this.route || route.length > this.route.length) {this.route = route;}\n }\n this.setLocationData(mediatorData);\n if (!this.route && this.routes.defaultRoute) {this.route = 'defaultRoute';}\n if (this.route) {\n const selected = this.routes[this.route]!;\n if (typeof selected !== 'function' && selected.secure && selected.secure(this.scope, this.locationData) !== true) {\n return false;\n }\n const previous = this.previousRoute ? this.routes[this.previousRoute] : undefined;\n if (previous && typeof previous !== 'function' && typeof previous.view === 'object' && previous.view.destroy) {\n previous.view.destroy(this.scope, this.locationData);\n }\n if (typeof selected === 'function') {\n selected(this.scope, this.locationData);\n } else if (typeof selected.view === 'function') {\n selected.view(this.scope, this.locationData);\n } else if (selected.view && typeof selected.view.initialize === 'function') {\n selected.view.initialize(this.scope, this.locationData);\n } else {\n return false;\n }\n this.applyPageContext(selected);\n this.previousRoute = this.route;\n }\n if (!isPopState && this.isBrowserRuntime()) {\n window.history.pushState(this.url, this.pageTitle || '', this.url);\n }\n return this;\n }\n}\n\n/** Public route configuration and navigation payload types. */\nnamespace Router {\n export type Handler = RouteHandler;\n export type Route = RouteHandler | RouteObject;\n export type Location = LocationData;\n export type Navigation = NavigationData;\n}\nexport = Router;\n"]}
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 private currentNavigationRoot: Document | Element | null = null;\n private boundNavigationRoot: Document | Element | null = null;\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 get navigationRoot(): Document | Element | null {return this.currentNavigationRoot;}\n\n /** Scope progressive link interception and move an active click listener when the root changes. */\n set navigationRoot(value: Document | Element | null) {\n if (value === this.currentNavigationRoot) {return;}\n if (this.listenersInitialized && this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.currentNavigationRoot = value;\n const nextRoot = value || document;\n nextRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = nextRoot;\n return;\n }\n this.currentNavigationRoot = value;\n }\n\n /** True when browser navigation APIs are available. */\n private isBrowserRuntime() {\n return typeof window !== 'undefined' && typeof document !== 'undefined';\n }\n\n /** Stable base URL for WHATWG URL parsing in browser and server runtimes. */\n private getOrigin() {\n return this.isBrowserRuntime() ? window.location.origin : 'http://white-label.invalid';\n }\n\n /** Read the authoritative browser URL, or retain the supplied server URL. */\n private getCurrentUrl() {\n if (!this.isBrowserRuntime()) {return this.url || '/';}\n return window.location.pathname + window.location.search + (window.location.hash || '');\n }\n\n /** 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 const navigationRoot = this.currentNavigationRoot || document;\n navigationRoot.addEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = navigationRoot;\n window.addEventListener('popstate', this.boundPopState);\n }\n if (this.currentMediator) {\n this.currentMediator.addEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = this.currentMediator;\n }\n this.listenersInitialized = true;\n return this;\n }\n\n /** Release owned state and listeners so the instance can leave the application lifecycle. */\n destroy() {\n this.removeListeners();\n return this;\n }\n\n /** Release listeners owned by this instance; subclasses may extend the lifecycle hook. */\n removeListeners() {\n if (!this.listenersInitialized) {return this;}\n if (this.isBrowserRuntime()) {\n this.boundNavigationRoot?.removeEventListener('click', this.boundPushStateClick as EventListener);\n this.boundNavigationRoot = null;\n window.removeEventListener('popstate', this.boundPopState);\n }\n if (this.boundMediator) {\n this.boundMediator.removeEventListener('router:navigate', this.boundMediatorNavigate);\n this.boundMediator = false;\n }\n this.listenersInitialized = false;\n return this;\n }\n\n /** Intercept eligible same-origin push-state links while preserving normal browser actions. */\n eventPushStateClick(e: MouseEvent) {\n if (typeof window === 'undefined') {return true;}\n if (e.defaultPrevented || (e.button !== undefined && e.button !== 0) || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return true;\n }\n const eventTarget = e.target as Element | null;\n const source = eventTarget && (typeof eventTarget.closest === 'function' ? eventTarget : eventTarget.parentElement);\n const anchor = source && typeof source.closest === 'function' ? source.closest('a[href][data-pushstate]') : null;\n if (!anchor) {return true;}\n const target = anchor.getAttribute('target');\n if (anchor.hasAttribute('download') || (target && target.toLowerCase() !== '_self')) {return true;}\n const 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,12 +1,13 @@
1
1
  {
2
2
  "name": "white-label-router",
3
- "version": "5.0.0",
4
- "description": "Framework-independent TypeScript router for browser, server, and request-scoped Node.js functions with progressive enhancement, guards, and accessible navigation.",
3
+ "version": "6.1.0",
4
+ "description": "Framework-independent TypeScript router for progressive enhancement in browser, server-rendered, and Node.js applications, with scoped links, guards, and accessible navigation.",
5
5
  "sideEffects": false,
6
6
  "main": "dist/index.js",
7
7
  "files": [
8
8
  "dist",
9
- "README.md"
9
+ "README.md",
10
+ "PACKAGE_MANAGERS.md"
10
11
  ],
11
12
  "engines": {
12
13
  "node": "^22.18.0 || >=24.11.0"
@@ -29,6 +30,9 @@
29
30
  "client-side-routing",
30
31
  "history-api",
31
32
  "progressive-enhancement",
33
+ "incremental-adoption",
34
+ "html-first",
35
+ "server-rendered",
32
36
  "navigation",
33
37
  "accessibility",
34
38
  "framework-independent",