lit-navigation-router 0.3.0 → 0.5.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.
@@ -5,6 +5,26 @@
5
5
  *
6
6
  * Modifications Copyright 2026 VanLandingham Labs, same license. See NOTICE.md.
7
7
  */
8
+ /**
9
+ * Splits `path` into the components a pattern is matched against.
10
+ *
11
+ * Deliberately not `new URL(path, origin)`: a nested controller's path is a
12
+ * *tail* — a bare relative segment like `abc` — which `URL` would resolve
13
+ * against the current directory and mangle.
14
+ */
15
+ const parseLocation = (path) => {
16
+ const hashIndex = path.indexOf('#');
17
+ const hash = hashIndex === -1 ? '' : path.slice(hashIndex + 1);
18
+ const beforeHash = hashIndex === -1 ? path : path.slice(0, hashIndex);
19
+ const searchIndex = beforeHash.indexOf('?');
20
+ return {
21
+ pathname: searchIndex === -1 ? beforeHash : beforeHash.slice(0, searchIndex),
22
+ search: searchIndex === -1 ? '' : beforeHash.slice(searchIndex + 1),
23
+ hash,
24
+ };
25
+ };
26
+ /** Re-attaches `search` and `hash` to a pathname. Inverse of `parseLocation`. */
27
+ const formatLocation = (pathname, { search, hash }) => pathname + (search === '' ? '' : `?${search}`) + (hash === '' ? '' : `#${hash}`);
8
28
  // A cache of URLPatterns created for PathRouteConfig.
9
29
  // Rather than converting all given RoutConfigs to URLPatternRouteConfig, this
10
30
  // lets us make `routes` mutable so users can add new PathRouteConfigs
@@ -21,11 +41,42 @@ const getPattern = (route) => {
21
41
  }
22
42
  return pattern;
23
43
  };
24
- // The implicit "/*" pattern every configured fallback matches against. Built on
25
- // first use rather than at module scope so that importing this module never
26
- // touches `URLPattern` — it may be polyfilled after import, or absent entirely.
27
- let wildcardPattern;
28
- const getWildcardPattern = () => (wildcardPattern ??= new URLPattern({ pathname: '/*' }));
44
+ /**
45
+ * Matches a pathname pattern that ends in a wildcard, in the forms
46
+ * `URLPattern.prototype.pathname` regenerates one: a bare `*` — which a
47
+ * trailing `(.*)` also normalises to — or `{*}`, which the generator emits for
48
+ * a wildcard after a modified group, e.g. `/docs{/}?*`. Either may be
49
+ * optional (`*?`). Not a wildcard: an escaped `\*`, or a `*` that is the
50
+ * modifier on a group (`(\d+)*`, `{/}*`) or on a named param (`:rest*`).
51
+ * Those exclusions matter when an earlier positional group exists —
52
+ * `/x/(\d+)/:rest*` — since that group would otherwise be taken for the tail.
53
+ */
54
+ const TRAILING_WILDCARD = /(?:(?<![\\)}]|:[\w$]+)\*|\{\*\})\??$/;
55
+ /**
56
+ * The tail of a match — what a trailing wildcard (`/foo/*`) captured — or
57
+ * undefined when the pattern has none.
58
+ *
59
+ * Decided from the pattern, not from the groups object: an unnamed regex group
60
+ * (`/post/(\d+)`) and a wildcard that is not last (`/foo/*` followed by
61
+ * `/bar`) are keyed by index exactly as a tail is, and reading either as one
62
+ * truncated `link()` and handed a child the wrong segment. When a trailing
63
+ * wildcard is present it is the last group in the pattern, so its key is the
64
+ * highest positional index.
65
+ */
66
+ const tailOf = (route, params) => {
67
+ if (!TRAILING_WILDCARD.test(getPattern(route).pathname)) {
68
+ return undefined;
69
+ }
70
+ let tailIndex = -1;
71
+ for (const key of Object.keys(params)) {
72
+ // Numeric, not lexicographic: '9' sorts above '10' as a string, so a
73
+ // pattern with eleven or more wildcards picked group 9 as its tail.
74
+ if (/^\d+$/.test(key) && Number(key) > tailIndex) {
75
+ tailIndex = Number(key);
76
+ }
77
+ }
78
+ return tailIndex < 0 ? undefined : params[String(tailIndex)];
79
+ };
29
80
  /**
30
81
  * A reactive controller that performs location-based routing using a
31
82
  * configuration of URL patterns and associated render callbacks.
@@ -51,7 +102,10 @@ export class Routes {
51
102
  routes = [];
52
103
  /**
53
104
  * A default fallback route which will always be matched if none of the
54
- * {@link routes} match. Implicitly matches to the path "/*".
105
+ * {@link routes} match. Behaves like a `/*` route: `params[0]` is the whole
106
+ * pathname minus its leading slash, and is handed to child controllers as
107
+ * their tail. A nested controller's own pathname is a tail, with no leading
108
+ * slash; the fallback accepts that too.
55
109
  */
56
110
  fallback;
57
111
  /*
@@ -70,6 +124,16 @@ export class Routes {
70
124
  /** Monotonic goto counter; see the last-goto-wins note in goto(). */
71
125
  _gotoSeq = 0;
72
126
  _currentPathname;
127
+ _currentTail;
128
+ /**
129
+ * The search and hash of the current location.
130
+ *
131
+ * Ambient rather than tailed: only the pathname nests, so a child controller
132
+ * is handed the parent's tail as its pathname but the *same* search and
133
+ * hash. There is no meaningful way to split a fragment across a route tree.
134
+ */
135
+ _currentSearch = '';
136
+ _currentHash = '';
73
137
  _currentRoute;
74
138
  _currentParams = {};
75
139
  /**
@@ -113,12 +177,10 @@ export class Routes {
113
177
  * and the outlet ends up on a route the URL has already left. `Router`
114
178
  * threads `NavigateEvent.signal` through for exactly this reason.
115
179
  */
116
- async goto(pathname, options) {
180
+ async goto(path, options) {
117
181
  // TODO (justinfagnani): handle absolute vs relative paths separately.
118
- // TODO (justinfagnani): generalize this to handle query params and
119
- // fragments. It currently only handles path names because it's easier to
120
- // completely disregard the origin for now. The click handler only does
121
- // an in-page navigation if the origin matches anyway.
182
+ const location = parseLocation(path);
183
+ const { pathname } = location;
122
184
  // Last-goto-wins, per controller. The navigation signal alone is not
123
185
  // enough: a child controller mounts as a *result* of its parent's render,
124
186
  // so its first goto() comes from `_onRoutesConnected` — after the parent's
@@ -127,23 +189,23 @@ export class Routes {
127
189
  // newer one. This also keeps `Routes` correct when used on its own, with
128
190
  // no `Router` and no Navigation API in the picture.
129
191
  const seq = ++this._gotoSeq;
130
- let tailGroup;
192
+ let tail;
131
193
  if (this.routes.length === 0 && this.fallback === undefined) {
132
194
  // If a routes controller has none of its own routes it acts like it has
133
195
  // one route of `/*` so that it passes the whole pathname as a tail
134
196
  // match.
135
- tailGroup = pathname;
197
+ tail = pathname;
136
198
  this._currentPathname = '';
137
199
  // Simulate a tail group with the whole pathname
138
- this._currentParams = { 0: tailGroup };
200
+ this._currentParams = { 0: tail };
139
201
  }
140
202
  else {
141
- const match = this._match(pathname);
203
+ const match = this._match(location);
142
204
  if (match === undefined) {
143
- throw new Error(`No route found for ${pathname}`);
205
+ throw new Error(`No route found for ${path}`);
144
206
  }
145
207
  const { route, params } = match;
146
- tailGroup = getTailGroup(params);
208
+ tail = match.tail;
147
209
  if (typeof route.enter === 'function') {
148
210
  const success = await route.enter(params);
149
211
  // If enter() returns false, cancel this navigation
@@ -160,10 +222,13 @@ export class Routes {
160
222
  this._currentRoute = route;
161
223
  this._currentParams = params;
162
224
  this._currentPathname =
163
- tailGroup === undefined
225
+ tail === undefined
164
226
  ? pathname
165
- : pathname.substring(0, pathname.length - tailGroup.length);
227
+ : pathname.substring(0, pathname.length - tail.length);
166
228
  }
229
+ this._currentTail = tail;
230
+ this._currentSearch = location.search;
231
+ this._currentHash = location.hash;
167
232
  // Propagate the tail match to children — deliberately NOT awaited.
168
233
  //
169
234
  // Awaiting looks like it would make `navigation.finished` cover the whole
@@ -177,10 +242,13 @@ export class Routes {
177
242
  // committed, outlet stranded, i.e. this fork's own thesis bug one level
178
243
  // down. Nested supersession is handled by the goto counter above, not by
179
244
  // awaiting. `_routeChild` covers the per-child filtering and error policy.
180
- if (tailGroup !== undefined) {
181
- for (const childRoutes of this._childRoutes) {
182
- this._routeChild(childRoutes, tailGroup);
183
- }
245
+ //
246
+ // Runs whether or not there is a tail. A route without one has nothing for
247
+ // the children to render, but they must still be superseded — otherwise a
248
+ // child mid-`enter()` for the previous tail stays current and commits over
249
+ // a URL that has moved on.
250
+ for (const childRoutes of this._childRoutes) {
251
+ this._routeChild(childRoutes, tail);
184
252
  }
185
253
  this._host.requestUpdate();
186
254
  }
@@ -207,7 +275,9 @@ export class Routes {
207
275
  * a genuine `enter()` rejection still surfaces the way it does upstream.
208
276
  * Skipping must still supersede: `goto()` is where the counter is bumped, so
209
277
  * returning without it would leave an in-flight child navigation current,
210
- * free to commit over a URL that has moved on.
278
+ * free to commit over a URL that has moved on. A parent route with no tail
279
+ * at all is the same case: nothing to route, but still something to stand
280
+ * down.
211
281
  *
212
282
  * No abort signal is threaded through, and the goto is deliberately not
213
283
  * awaited. The parent commits its own state before children run, so a child
@@ -217,11 +287,19 @@ export class Routes {
217
287
  * replacement. Supersession is the counter's job.
218
288
  */
219
289
  _routeChild(child, tail) {
220
- if (!child.hasRouteFor(tail)) {
290
+ if (tail === undefined) {
221
291
  child._supersede();
222
292
  return;
223
293
  }
224
- void child.goto(tail).catch((err) => {
294
+ const childPath = formatLocation(tail, {
295
+ search: this._currentSearch,
296
+ hash: this._currentHash,
297
+ });
298
+ if (!child.hasRouteFor(childPath)) {
299
+ child._supersede();
300
+ return;
301
+ }
302
+ void child.goto(childPath).catch((err) => {
225
303
  queueMicrotask(() => {
226
304
  throw err;
227
305
  });
@@ -252,7 +330,29 @@ export class Routes {
252
330
  }
253
331
  }
254
332
  /**
255
- * True when this controller can render `pathname` — i.e. a route matches, or
333
+ * True when this controller, or any controller below it, has a route that
334
+ * constrains the hash.
335
+ *
336
+ * `Router` gates interception of fragment-only navigation on this. Left
337
+ * ungated, a pathname-only app would have every in-page anchor swallowed and
338
+ * re-rendered instead of scrolled; gated, such an app behaves exactly as it
339
+ * did before hash routes existed.
340
+ *
341
+ * Walks children because a nested controller may route on the hash while the
342
+ * top-level `Router` does not — and only the top-level one sees the
343
+ * navigate event.
344
+ */
345
+ _constrainsHash(seen = new Set()) {
346
+ // Cycle guard, matching `_supersede`; see the note there.
347
+ if (seen.has(this)) {
348
+ return false;
349
+ }
350
+ seen.add(this);
351
+ return (this.routes.some((r) => getPattern(r).hash !== '*') ||
352
+ this._childRoutes.some((c) => c._constrainsHash(seen)));
353
+ }
354
+ /**
355
+ * True when this controller can render `path` — i.e. a route matches, or
256
356
  * a fallback is configured.
257
357
  *
258
358
  * `Router` gates interception on this: intercepting a path we cannot render
@@ -260,7 +360,7 @@ export class Routes {
260
360
  * moved and the outlet stale. Letting the browser handle it instead means a
261
361
  * server-rendered page, an export endpoint, or a GET form still works.
262
362
  */
263
- hasRouteFor(pathname) {
363
+ hasRouteFor(path) {
264
364
  // A fallback matches everything, and a controller with no routes of its own
265
365
  // behaves as if it had a single `/*` route (goto()'s special case). Either
266
366
  // way the answer is yes without running a single pattern — worth
@@ -270,7 +370,8 @@ export class Routes {
270
370
  }
271
371
  // `test()`, not `_match()`: this only needs the yes/no, and `exec()` pays
272
372
  // ~8x on a hit to build a groups object the caller would throw away.
273
- return this.routes.some((r) => getPattern(r).test({ pathname }));
373
+ const location = parseLocation(path);
374
+ return this.routes.some((r) => getPattern(r).test(location));
274
375
  }
275
376
  /**
276
377
  * Matches `pathname` against the installed routes and returns the first match
@@ -280,26 +381,39 @@ export class Routes {
280
381
  * extract: that ran the winning pattern twice, and every caller that wants a
281
382
  * route wants its params too.
282
383
  */
283
- _match(pathname) {
384
+ _match(location) {
284
385
  for (const route of this.routes) {
285
- const result = getPattern(route).exec({ pathname });
386
+ const result = getPattern(route).exec(location);
286
387
  if (result !== null) {
287
- return { route, params: result.pathname.groups };
388
+ const params = {
389
+ ...result.pathname.groups,
390
+ };
391
+ // Named groups only. A positional group is keyed by index in every
392
+ // component independently, so `/child/*` with a hash of `*` yields a
393
+ // "0" in both — and `tailOf` picks the tail by highest numeric key.
394
+ // Merging them would let a fragment masquerade as the tail.
395
+ for (const groups of [result.search.groups, result.hash.groups]) {
396
+ for (const [key, value] of Object.entries(groups)) {
397
+ if (!/^\d+$/.test(key)) {
398
+ params[key] = value;
399
+ }
400
+ }
401
+ }
402
+ return { route, params, tail: tailOf(route, result.pathname.groups) };
288
403
  }
289
404
  }
290
405
  if (this.fallback === undefined) {
291
406
  return undefined;
292
407
  }
293
408
  // The fallback route behaves like it has a "/*" path. This is hidden from
294
- // the public API but is added here to return a valid RouteConfig. The
295
- // pattern is the shared one rather than one derived from this object:
296
- // the spread produces a fresh object every call, which `patternCache` —
297
- // keyed by identity — would miss, rebuilding a URLPattern per navigation.
298
- const wildcard = getWildcardPattern();
299
- return {
300
- route: { ...this.fallback, path: '/*' },
301
- params: wildcard.exec({ pathname })?.pathname.groups ?? {},
302
- };
409
+ // the public API; the `path` is there to return a valid RouteConfig. The
410
+ // match itself is done by hand rather than with a real `/*` pattern: a
411
+ // nested controller is handed its tail *without* a leading slash, which
412
+ // `/*` does not match, so a nested fallback matched nothing — empty
413
+ // params, no tail, and its own children never routed.
414
+ const { pathname } = location;
415
+ const tail = pathname.startsWith('/') ? pathname.slice(1) : pathname;
416
+ return { route: { ...this.fallback, path: '/*' }, params: { 0: tail }, tail };
303
417
  }
304
418
  hostConnected() {
305
419
  this._host.addEventListener(RoutesConnectedEvent.eventName, this._onRoutesConnected);
@@ -339,41 +453,11 @@ export class Routes {
339
453
  };
340
454
  // A child that mounts under an existing tail match has to be caught up to
341
455
  // it — it missed the propagation loop in goto() that ran before it existed.
342
- const tailGroup = getTailGroup(this._currentParams);
343
- if (tailGroup !== undefined) {
344
- this._routeChild(childRoutes, tailGroup);
345
- }
456
+ // With no tail there is nothing to catch up to, and `_routeChild` then only
457
+ // supersedes, a no-op on a freshly mounted child.
458
+ this._routeChild(childRoutes, this._currentTail);
346
459
  };
347
460
  }
348
- /**
349
- * Returns the tail of a pathname groups object. This is the match from a
350
- * wildcard at the end of a pathname pattern, like `/foo/*`
351
- */
352
- const getTailGroup = (groups) => {
353
- let tailIndex = -1;
354
- for (const key of Object.keys(groups)) {
355
- // Anchored. `URLPattern` keys a positional group by its index, so a
356
- // non-digit key is never one — an unanchored test also accepts a *named*
357
- // group containing a digit (`:id2`), and since a letter sorts above a
358
- // digit it then won the comparison below and the param value was handed
359
- // to the child instead of the tail.
360
- //
361
- // Necessary, not sufficient: an unnamed *regex* group is positional too
362
- // (`/post/(\d+)` yields key "0"), as is a wildcard that is not last
363
- // (`/foo/*/bar`). Both are mis-read as tails here, and both predate this
364
- // check — selecting the tail properly needs the pattern, not just groups.
365
- if (!/^\d+$/.test(key)) {
366
- continue;
367
- }
368
- // Numeric, not lexicographic: '9' sorts above '10' as a string, so a
369
- // pattern with eleven or more wildcards picked group 9 as its tail.
370
- const index = Number(key);
371
- if (index > tailIndex) {
372
- tailIndex = index;
373
- }
374
- }
375
- return tailIndex < 0 ? undefined : groups[String(tailIndex)];
376
- };
377
461
  /**
378
462
  * This event is fired from Routes controllers when their host is connected to
379
463
  * announce the child route and potentially connect to a parent routes controller.
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Modifications Copyright 2026 VanLandingham Labs, same license. See NOTICE.md.\n */\n\n/// <reference types=\"urlpattern-polyfill\" />\n\nimport type {ReactiveController, ReactiveControllerHost} from 'lit';\n\nexport interface BaseRouteConfig {\n name?: string | undefined;\n render?: (params: {[key: string]: string | undefined}) => unknown;\n enter?: (params: {\n [key: string]: string | undefined;\n }) => Promise<boolean> | boolean;\n}\n\n/**\n * A RouteConfig that matches against a `path` string. `path` must be a\n * [`URLPattern` compatible pathname pattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/pathname).\n */\nexport interface PathRouteConfig extends BaseRouteConfig {\n path: string;\n}\n\n/**\n * A RouteConfig that matches against a given [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)\n *\n * While `URLPattern` can match against protocols, hostnames, and ports,\n * routes will only be checked for matches if they're part of the current\n * origin. This means that the pattern is limited to checking `pathname` and\n * `search`.\n */\nexport interface URLPatternRouteConfig extends BaseRouteConfig {\n pattern: URLPatternLike;\n}\n\n/**\n * The part of `URLPattern` this router uses.\n *\n * Declared structurally rather than referencing the global so the emitted\n * `.d.ts` is self-contained: the `/// <reference types=\"urlpattern-polyfill\" />`\n * above is not carried into declaration output, and `URLPattern` is not in\n * TypeScript's bundled `lib.dom`, so a published package typed against the\n * global fails a consumer build with `TS2304: Cannot find name 'URLPattern'`.\n * A real `URLPattern` satisfies this, so passing one still type-checks.\n */\nexport interface URLPatternLike {\n test(input: {pathname: string}): boolean;\n exec(input: {pathname: string}): {\n pathname: {groups: {[key: string]: string | undefined}};\n } | null;\n}\n\n/**\n * A description of a route, which path or pattern to match against, and a\n * render() callback used to render a match to the outlet.\n */\nexport type RouteConfig = PathRouteConfig | URLPatternRouteConfig;\n\n// A cache of URLPatterns created for PathRouteConfig.\n// Rather than converting all given RoutConfigs to URLPatternRouteConfig, this\n// lets us make `routes` mutable so users can add new PathRouteConfigs\n// dynamically.\nconst patternCache = new WeakMap<PathRouteConfig, URLPatternLike>();\n\nconst isPatternConfig = (route: RouteConfig): route is URLPatternRouteConfig =>\n (route as URLPatternRouteConfig).pattern !== undefined;\n\nconst getPattern = (route: RouteConfig): URLPatternLike => {\n if (isPatternConfig(route)) {\n return route.pattern;\n }\n let pattern = patternCache.get(route);\n if (pattern === undefined) {\n patternCache.set(route, (pattern = new URLPattern({pathname: route.path})));\n }\n return pattern;\n};\n\n// The implicit \"/*\" pattern every configured fallback matches against. Built on\n// first use rather than at module scope so that importing this module never\n// touches `URLPattern` — it may be polyfilled after import, or absent entirely.\nlet wildcardPattern: URLPatternLike | undefined;\n\nconst getWildcardPattern = (): URLPatternLike =>\n (wildcardPattern ??= new URLPattern({pathname: '/*'}));\n\n/**\n * A reactive controller that performs location-based routing using a\n * configuration of URL patterns and associated render callbacks.\n */\nexport class Routes implements ReactiveController {\n private readonly _host: ReactiveControllerHost & HTMLElement;\n\n /*\n * The currently installed set of routes in precedence order.\n *\n * This array is mutable. To dynamically add a new route you can write:\n *\n * ```ts\n * this._routes.routes.push({\n * path: '/foo',\n * render: () => html`<p>Foo</p>`,\n * });\n * ```\n *\n * Mutating this property does not trigger any route transitions. If the\n * changes may result is a different route matching for the current path, you\n * must instigate a route update with `goto()`.\n */\n routes: Array<RouteConfig> = [];\n\n /**\n * A default fallback route which will always be matched if none of the\n * {@link routes} match. Implicitly matches to the path \"/*\".\n */\n fallback?: BaseRouteConfig;\n\n /*\n * The current set of child Routes controllers. These are connected via\n * the routes-connected event.\n */\n private readonly _childRoutes: Array<Routes> = [];\n\n private _parentRoutes: Routes | undefined;\n\n /*\n * State related to the current matching route.\n *\n * We keep this so that consuming code can access current parameters, and so\n * that we can propagate tail matches to child routes if they are added after\n * navigation / matching.\n */\n /** Monotonic goto counter; see the last-goto-wins note in goto(). */\n private _gotoSeq = 0;\n\n private _currentPathname: string | undefined;\n private _currentRoute: RouteConfig | undefined;\n private _currentParams: {\n [key: string]: string | undefined;\n } = {};\n\n /**\n * Callback to call when this controller is disconnected.\n *\n * It's critical to call this immediately in hostDisconnected so that this\n * controller instance doesn't receive a tail match meant for another route.\n */\n // TODO (justinfagnani): Do we need this now that we have a direct reference\n // to the parent? We can call `this._parentRoutes.disconnect(this)`.\n private _onDisconnect: (() => void) | undefined;\n\n constructor(\n host: ReactiveControllerHost & HTMLElement,\n routes: Array<RouteConfig>,\n options?: {fallback?: BaseRouteConfig}\n ) {\n (this._host = host).addController(this);\n this.routes = [...routes];\n this.fallback = options?.fallback;\n }\n\n /**\n * Returns a URL string of the current route, including parent routes,\n * optionally replacing the local path with `pathname`.\n */\n link(pathname?: string): string {\n if (pathname?.startsWith('/')) {\n return pathname;\n }\n if (pathname?.startsWith('.')) {\n throw new Error('Not implemented');\n }\n pathname ??= this._currentPathname;\n return (this._parentRoutes?.link() ?? '') + pathname;\n }\n\n /**\n * Navigates this routes controller to `pathname`.\n *\n * This does not navigate parent routes, so it isn't (yet) a general page\n * navigation API. It does navigate child routes if pathname matches a\n * pattern with a tail wildcard pattern (`/*`).\n *\n * Pass `options.signal` to make the navigation abandonable. `enter()` is\n * awaited, so a second `goto()` can start — and finish — while the first is\n * still resolving its route; without a signal the slower one commits last\n * and the outlet ends up on a route the URL has already left. `Router`\n * threads `NavigateEvent.signal` through for exactly this reason.\n */\n async goto(pathname: string, options?: {signal?: AbortSignal}) {\n // TODO (justinfagnani): handle absolute vs relative paths separately.\n\n // TODO (justinfagnani): generalize this to handle query params and\n // fragments. It currently only handles path names because it's easier to\n // completely disregard the origin for now. The click handler only does\n // an in-page navigation if the origin matches anyway.\n // Last-goto-wins, per controller. The navigation signal alone is not\n // enough: a child controller mounts as a *result* of its parent's render,\n // so its first goto() comes from `_onRoutesConnected` — after the parent's\n // navigation has already finished, and therefore with a signal that will\n // never abort. Without this counter a slow first child load commits over a\n // newer one. This also keeps `Routes` correct when used on its own, with\n // no `Router` and no Navigation API in the picture.\n const seq = ++this._gotoSeq;\n let tailGroup: string | undefined;\n\n if (this.routes.length === 0 && this.fallback === undefined) {\n // If a routes controller has none of its own routes it acts like it has\n // one route of `/*` so that it passes the whole pathname as a tail\n // match.\n tailGroup = pathname;\n this._currentPathname = '';\n // Simulate a tail group with the whole pathname\n this._currentParams = {0: tailGroup};\n } else {\n const match = this._match(pathname);\n if (match === undefined) {\n throw new Error(`No route found for ${pathname}`);\n }\n const {route, params} = match;\n tailGroup = getTailGroup(params);\n if (typeof route.enter === 'function') {\n const success = await route.enter(params);\n // If enter() returns false, cancel this navigation\n if (success === false) {\n return;\n }\n }\n // A newer navigation superseded this one while `enter` was awaiting.\n // Committing now would swap the outlet onto a route the URL has left.\n if (options?.signal?.aborted === true || seq !== this._gotoSeq) {\n return;\n }\n // Only update route state if the enter handler completes successfully\n this._currentRoute = route;\n this._currentParams = params;\n this._currentPathname =\n tailGroup === undefined\n ? pathname\n : pathname.substring(0, pathname.length - tailGroup.length);\n }\n\n // Propagate the tail match to children — deliberately NOT awaited.\n //\n // Awaiting looks like it would make `navigation.finished` cover the whole\n // tree, and an earlier revision of this fork did it. It is wrong twice\n // over. At this point `requestUpdate()` has not run, so `_childRoutes`\n // still holds the *outgoing* branch's controller: awaiting it gates the\n // parent's outlet swap on an `enter()` for a tail that controller will\n // never render (a hung one blocks the navigation forever), and if that\n // child has no route for the new tail its `No route found` throw\n // propagates out of here and `requestUpdate()` below never runs — URL\n // committed, outlet stranded, i.e. this fork's own thesis bug one level\n // down. Nested supersession is handled by the goto counter above, not by\n // awaiting. `_routeChild` covers the per-child filtering and error policy.\n if (tailGroup !== undefined) {\n for (const childRoutes of this._childRoutes) {\n this._routeChild(childRoutes, tailGroup);\n }\n }\n this._host.requestUpdate();\n }\n\n /**\n * The result of calling the current route's render() callback.\n */\n outlet() {\n return this._currentRoute?.render?.(this._currentParams);\n }\n\n /**\n * The current parsed route parameters.\n */\n get params() {\n return this._currentParams;\n }\n\n /**\n * Hands a tail match to a child controller. Shared by the propagation loop in\n * `goto()` and the late-mount path in `_onRoutesConnected`, so that identical\n * input cannot be silent on one and an uncaught global throw on the other.\n *\n * A child with no route for the new tail is the expected case, not an error —\n * the outgoing branch mid-swap, or a deep link to a path the child cannot\n * render. Filtered structurally rather than by swallowing every rejection, so\n * a genuine `enter()` rejection still surfaces the way it does upstream.\n * Skipping must still supersede: `goto()` is where the counter is bumped, so\n * returning without it would leave an in-flight child navigation current,\n * free to commit over a URL that has moved on.\n *\n * No abort signal is threaded through, and the goto is deliberately not\n * awaited. The parent commits its own state before children run, so a child\n * handed an already-aborted signal stands down with no newer goto() arriving\n * to correct it, leaving the nested outlet stuck — reachable, because a\n * hash-only navigation aborts the outstanding one without producing a\n * replacement. Supersession is the counter's job.\n */\n private _routeChild(child: Routes, tail: string) {\n if (!child.hasRouteFor(tail)) {\n child._supersede();\n return;\n }\n void child.goto(tail).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n }\n\n /**\n * Invalidate any in-flight `goto()` on this controller without starting a\n * new one. Same-class access, so `_gotoSeq` stays private to `Routes`.\n */\n private _supersede(seen: Set<Routes> = new Set()): void {\n // Unreachable defence in depth. Upstream *can* produce a `_childRoutes`\n // cycle — a host carrying two Routes controllers, disconnected and\n // reconnected, ends up with each registered as the other's child — but\n // `hostDisconnected` below removes the listener that causes it, and a test\n // asserts the cycle cannot form. Kept because an unguarded recursive walk\n // over a cycle is a stack overflow rather than a misrender.\n if (seen.has(this)) {\n return;\n }\n seen.add(this);\n this._gotoSeq++;\n // Recursive: on the navigating branch the child's own propagation loop\n // reaches the grandchildren, but a skipped child never runs one — so\n // without this an in-flight grandchild `enter()` stays current and commits\n // over a URL that has moved on, the same defect one level deeper.\n for (const child of this._childRoutes) {\n child._supersede(seen);\n }\n }\n\n /**\n * True when this controller can render `pathname` — i.e. a route matches, or\n * a fallback is configured.\n *\n * `Router` gates interception on this: intercepting a path we cannot render\n * commits the URL and then throws out of `goto()`, leaving the address bar\n * moved and the outlet stale. Letting the browser handle it instead means a\n * server-rendered page, an export endpoint, or a GET form still works.\n */\n hasRouteFor(pathname: string): boolean {\n // A fallback matches everything, and a controller with no routes of its own\n // behaves as if it had a single `/*` route (goto()'s special case). Either\n // way the answer is yes without running a single pattern — worth\n // short-circuiting, since `Router` asks this on every navigation.\n if (this.fallback !== undefined || this.routes.length === 0) {\n return true;\n }\n // `test()`, not `_match()`: this only needs the yes/no, and `exec()` pays\n // ~8x on a hit to build a groups object the caller would throw away.\n return this.routes.some((r) => getPattern(r).test({pathname}));\n }\n\n /**\n * Matches `pathname` against the installed routes and returns the first match\n * with its parsed parameters, or the fallback's match if one is configured.\n *\n * One `exec()` per candidate rather than `test()` to select and `exec()` to\n * extract: that ran the winning pattern twice, and every caller that wants a\n * route wants its params too.\n */\n private _match(pathname: string):\n | {route: RouteConfig; params: {[key: string]: string | undefined}}\n | undefined {\n for (const route of this.routes) {\n const result = getPattern(route).exec({pathname});\n if (result !== null) {\n return {route, params: result.pathname.groups};\n }\n }\n if (this.fallback === undefined) {\n return undefined;\n }\n // The fallback route behaves like it has a \"/*\" path. This is hidden from\n // the public API but is added here to return a valid RouteConfig. The\n // pattern is the shared one rather than one derived from this object:\n // the spread produces a fresh object every call, which `patternCache` —\n // keyed by identity — would miss, rebuilding a URLPattern per navigation.\n const wildcard = getWildcardPattern();\n return {\n route: {...this.fallback, path: '/*'},\n params: wildcard.exec({pathname})?.pathname.groups ?? {},\n };\n }\n\n hostConnected() {\n this._host.addEventListener(\n RoutesConnectedEvent.eventName,\n this._onRoutesConnected\n );\n const event = new RoutesConnectedEvent(this);\n this._host.dispatchEvent(event);\n this._onDisconnect = event.onDisconnect;\n }\n\n hostDisconnected() {\n // Remove the listener hostConnected added. Without this a host that is\n // disconnected and reconnected (a repeat() reorder, a tab swap) leaves the\n // sibling controller's listener installed, so on the second connect it\n // claims the re-dispatching controller as *its* child and the pair point\n // at each other — a real `_childRoutes` cycle, which recursive walks turn\n // into a stack overflow.\n this._host.removeEventListener(\n RoutesConnectedEvent.eventName,\n this._onRoutesConnected\n );\n // When this child routes controller is disconnected because a parent\n // outlet rendered a different template, disconnecting will ensure that\n // this controller doesn't receive a tail match meant for another route.\n this._onDisconnect?.();\n this._parentRoutes = undefined;\n }\n\n private _onRoutesConnected = (e: RoutesConnectedEvent) => {\n // Don't handle the event fired by this routes controller, which we get\n // because we do this.dispatchEvent(...)\n if (e.routes === this) {\n return;\n }\n\n const childRoutes = e.routes;\n this._childRoutes.push(childRoutes);\n childRoutes._parentRoutes = this;\n\n e.stopImmediatePropagation();\n e.onDisconnect = () => {\n const index = this._childRoutes.indexOf(childRoutes);\n if (index !== -1) {\n this._childRoutes.splice(index, 1);\n }\n };\n\n // A child that mounts under an existing tail match has to be caught up to\n // it — it missed the propagation loop in goto() that ran before it existed.\n const tailGroup = getTailGroup(this._currentParams);\n if (tailGroup !== undefined) {\n this._routeChild(childRoutes, tailGroup);\n }\n };\n}\n\n/**\n * Returns the tail of a pathname groups object. This is the match from a\n * wildcard at the end of a pathname pattern, like `/foo/*`\n */\nconst getTailGroup = (groups: {[key: string]: string | undefined}) => {\n let tailIndex = -1;\n for (const key of Object.keys(groups)) {\n // Anchored. `URLPattern` keys a positional group by its index, so a\n // non-digit key is never one — an unanchored test also accepts a *named*\n // group containing a digit (`:id2`), and since a letter sorts above a\n // digit it then won the comparison below and the param value was handed\n // to the child instead of the tail.\n //\n // Necessary, not sufficient: an unnamed *regex* group is positional too\n // (`/post/(\\d+)` yields key \"0\"), as is a wildcard that is not last\n // (`/foo/*/bar`). Both are mis-read as tails here, and both predate this\n // check — selecting the tail properly needs the pattern, not just groups.\n if (!/^\\d+$/.test(key)) {\n continue;\n }\n // Numeric, not lexicographic: '9' sorts above '10' as a string, so a\n // pattern with eleven or more wildcards picked group 9 as its tail.\n const index = Number(key);\n if (index > tailIndex) {\n tailIndex = index;\n }\n }\n return tailIndex < 0 ? undefined : groups[String(tailIndex)];\n};\n\n/**\n * This event is fired from Routes controllers when their host is connected to\n * announce the child route and potentially connect to a parent routes controller.\n */\nexport class RoutesConnectedEvent extends Event {\n static readonly eventName = 'lit-routes-connected';\n readonly routes: Routes;\n onDisconnect?: () => void;\n\n constructor(routes: Routes) {\n super(RoutesConnectedEvent.eventName, {\n bubbles: true,\n composed: true,\n cancelable: false,\n });\n this.routes = routes;\n }\n}\n\ndeclare global {\n interface HTMLElementEventMap {\n [RoutesConnectedEvent.eventName]: RoutesConnectedEvent;\n }\n}\n"]}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Modifications Copyright 2026 VanLandingham Labs, same license. See NOTICE.md.\n */\n\n/// <reference types=\"urlpattern-polyfill\" />\n\nimport type {ReactiveController, ReactiveControllerHost} from 'lit';\n\nexport interface BaseRouteConfig {\n name?: string | undefined;\n render?: (params: {[key: string]: string | undefined}) => unknown;\n enter?: (params: {\n [key: string]: string | undefined;\n }) => Promise<boolean> | boolean;\n}\n\n/**\n * A RouteConfig that matches against a `path` string. `path` must be a\n * [`URLPattern` compatible pathname pattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/pathname).\n */\nexport interface PathRouteConfig extends BaseRouteConfig {\n path: string;\n}\n\n/**\n * A RouteConfig that matches against a given [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)\n *\n * While `URLPattern` can match against protocols, hostnames, and ports,\n * routes will only be checked for matches if they're part of the current\n * origin. This means the pattern is limited to checking `pathname`, `search`\n * and `hash`.\n */\nexport interface URLPatternRouteConfig extends BaseRouteConfig {\n pattern: URLPatternLike;\n}\n\n/**\n * The part of `URLPattern` this router uses.\n *\n * Declared structurally rather than referencing the global so the emitted\n * `.d.ts` is self-contained: the `/// <reference types=\"urlpattern-polyfill\" />`\n * above is not carried into declaration output, and `URLPattern` is not in\n * TypeScript's bundled `lib.dom`, so a published package typed against the\n * global fails a consumer build with `TS2304: Cannot find name 'URLPattern'`.\n * A real `URLPattern` satisfies this, so passing one still type-checks.\n */\nexport interface URLPatternLike {\n /**\n * The pathname pattern string, as `URLPattern.prototype.pathname` returns\n * it. Read to tell a trailing wildcard from any other positional group —\n * the groups object alone cannot (see `tailOf`).\n */\n readonly pathname: string;\n /**\n * The hash pattern string. `'*'` means the route places no constraint on the\n * fragment, which is what `URLPattern` fills in for any component the caller\n * omits. `Router` reads this to decide whether a fragment-only navigation is\n * its business or the browser's.\n */\n readonly hash: string;\n test(input: RouteLocation): boolean;\n exec(input: RouteLocation): {\n pathname: {groups: {[key: string]: string | undefined}};\n search: {groups: {[key: string]: string | undefined}};\n hash: {groups: {[key: string]: string | undefined}};\n } | null;\n}\n\n/**\n * The parts of a location this router matches against, split out of a path\n * string by `parseLocation`.\n *\n * `search` and `hash` carry no leading `?` or `#`. `URLPattern` canonicalises\n * either form away on an init input, but only for a real `URLPattern` — the\n * structural `URLPatternLike` above admits other implementations, so the\n * delimiters are stripped here rather than left to the pattern.\n */\nexport interface RouteLocation {\n pathname: string;\n search: string;\n hash: string;\n}\n\n/**\n * Splits `path` into the components a pattern is matched against.\n *\n * Deliberately not `new URL(path, origin)`: a nested controller's path is a\n * *tail* — a bare relative segment like `abc` — which `URL` would resolve\n * against the current directory and mangle.\n */\nconst parseLocation = (path: string): RouteLocation => {\n const hashIndex = path.indexOf('#');\n const hash = hashIndex === -1 ? '' : path.slice(hashIndex + 1);\n const beforeHash = hashIndex === -1 ? path : path.slice(0, hashIndex);\n const searchIndex = beforeHash.indexOf('?');\n return {\n pathname: searchIndex === -1 ? beforeHash : beforeHash.slice(0, searchIndex),\n search: searchIndex === -1 ? '' : beforeHash.slice(searchIndex + 1),\n hash,\n };\n};\n\n/** Re-attaches `search` and `hash` to a pathname. Inverse of `parseLocation`. */\nconst formatLocation = (\n pathname: string,\n {search, hash}: {search: string; hash: string}\n): string =>\n pathname + (search === '' ? '' : `?${search}`) + (hash === '' ? '' : `#${hash}`);\n\n/**\n * A description of a route, which path or pattern to match against, and a\n * render() callback used to render a match to the outlet.\n */\nexport type RouteConfig = PathRouteConfig | URLPatternRouteConfig;\n\n// A cache of URLPatterns created for PathRouteConfig.\n// Rather than converting all given RoutConfigs to URLPatternRouteConfig, this\n// lets us make `routes` mutable so users can add new PathRouteConfigs\n// dynamically.\nconst patternCache = new WeakMap<PathRouteConfig, URLPatternLike>();\n\nconst isPatternConfig = (route: RouteConfig): route is URLPatternRouteConfig =>\n (route as URLPatternRouteConfig).pattern !== undefined;\n\nconst getPattern = (route: RouteConfig): URLPatternLike => {\n if (isPatternConfig(route)) {\n return route.pattern;\n }\n let pattern = patternCache.get(route);\n if (pattern === undefined) {\n patternCache.set(route, (pattern = new URLPattern({pathname: route.path})));\n }\n return pattern;\n};\n\n/**\n * Matches a pathname pattern that ends in a wildcard, in the forms\n * `URLPattern.prototype.pathname` regenerates one: a bare `*` — which a\n * trailing `(.*)` also normalises to — or `{*}`, which the generator emits for\n * a wildcard after a modified group, e.g. `/docs{/}?*`. Either may be\n * optional (`*?`). Not a wildcard: an escaped `\\*`, or a `*` that is the\n * modifier on a group (`(\\d+)*`, `{/}*`) or on a named param (`:rest*`).\n * Those exclusions matter when an earlier positional group exists —\n * `/x/(\\d+)/:rest*` — since that group would otherwise be taken for the tail.\n */\nconst TRAILING_WILDCARD = /(?:(?<![\\\\)}]|:[\\w$]+)\\*|\\{\\*\\})\\??$/;\n\n/**\n * The tail of a match — what a trailing wildcard (`/foo/*`) captured — or\n * undefined when the pattern has none.\n *\n * Decided from the pattern, not from the groups object: an unnamed regex group\n * (`/post/(\\d+)`) and a wildcard that is not last (`/foo/*` followed by\n * `/bar`) are keyed by index exactly as a tail is, and reading either as one\n * truncated `link()` and handed a child the wrong segment. When a trailing\n * wildcard is present it is the last group in the pattern, so its key is the\n * highest positional index.\n */\nconst tailOf = (\n route: RouteConfig,\n params: {[key: string]: string | undefined}\n): string | undefined => {\n if (!TRAILING_WILDCARD.test(getPattern(route).pathname)) {\n return undefined;\n }\n let tailIndex = -1;\n for (const key of Object.keys(params)) {\n // Numeric, not lexicographic: '9' sorts above '10' as a string, so a\n // pattern with eleven or more wildcards picked group 9 as its tail.\n if (/^\\d+$/.test(key) && Number(key) > tailIndex) {\n tailIndex = Number(key);\n }\n }\n return tailIndex < 0 ? undefined : params[String(tailIndex)];\n};\n\n/**\n * A reactive controller that performs location-based routing using a\n * configuration of URL patterns and associated render callbacks.\n */\nexport class Routes implements ReactiveController {\n private readonly _host: ReactiveControllerHost & HTMLElement;\n\n /*\n * The currently installed set of routes in precedence order.\n *\n * This array is mutable. To dynamically add a new route you can write:\n *\n * ```ts\n * this._routes.routes.push({\n * path: '/foo',\n * render: () => html`<p>Foo</p>`,\n * });\n * ```\n *\n * Mutating this property does not trigger any route transitions. If the\n * changes may result is a different route matching for the current path, you\n * must instigate a route update with `goto()`.\n */\n routes: Array<RouteConfig> = [];\n\n /**\n * A default fallback route which will always be matched if none of the\n * {@link routes} match. Behaves like a `/*` route: `params[0]` is the whole\n * pathname minus its leading slash, and is handed to child controllers as\n * their tail. A nested controller's own pathname is a tail, with no leading\n * slash; the fallback accepts that too.\n */\n fallback?: BaseRouteConfig;\n\n /*\n * The current set of child Routes controllers. These are connected via\n * the routes-connected event.\n */\n private readonly _childRoutes: Array<Routes> = [];\n\n private _parentRoutes: Routes | undefined;\n\n /*\n * State related to the current matching route.\n *\n * We keep this so that consuming code can access current parameters, and so\n * that we can propagate tail matches to child routes if they are added after\n * navigation / matching.\n */\n /** Monotonic goto counter; see the last-goto-wins note in goto(). */\n private _gotoSeq = 0;\n\n private _currentPathname: string | undefined;\n private _currentTail: string | undefined;\n /**\n * The search and hash of the current location.\n *\n * Ambient rather than tailed: only the pathname nests, so a child controller\n * is handed the parent's tail as its pathname but the *same* search and\n * hash. There is no meaningful way to split a fragment across a route tree.\n */\n private _currentSearch = '';\n private _currentHash = '';\n private _currentRoute: RouteConfig | undefined;\n private _currentParams: {\n [key: string]: string | undefined;\n } = {};\n\n /**\n * Callback to call when this controller is disconnected.\n *\n * It's critical to call this immediately in hostDisconnected so that this\n * controller instance doesn't receive a tail match meant for another route.\n */\n // TODO (justinfagnani): Do we need this now that we have a direct reference\n // to the parent? We can call `this._parentRoutes.disconnect(this)`.\n private _onDisconnect: (() => void) | undefined;\n\n constructor(\n host: ReactiveControllerHost & HTMLElement,\n routes: Array<RouteConfig>,\n options?: {fallback?: BaseRouteConfig}\n ) {\n (this._host = host).addController(this);\n this.routes = [...routes];\n this.fallback = options?.fallback;\n }\n\n /**\n * Returns a URL string of the current route, including parent routes,\n * optionally replacing the local path with `pathname`.\n */\n link(pathname?: string): string {\n if (pathname?.startsWith('/')) {\n return pathname;\n }\n if (pathname?.startsWith('.')) {\n throw new Error('Not implemented');\n }\n pathname ??= this._currentPathname;\n return (this._parentRoutes?.link() ?? '') + pathname;\n }\n\n /**\n * Navigates this routes controller to `pathname`.\n *\n * This does not navigate parent routes, so it isn't (yet) a general page\n * navigation API. It does navigate child routes if pathname matches a\n * pattern with a tail wildcard pattern (`/*`).\n *\n * Pass `options.signal` to make the navigation abandonable. `enter()` is\n * awaited, so a second `goto()` can start — and finish — while the first is\n * still resolving its route; without a signal the slower one commits last\n * and the outlet ends up on a route the URL has already left. `Router`\n * threads `NavigateEvent.signal` through for exactly this reason.\n */\n async goto(path: string, options?: {signal?: AbortSignal}) {\n // TODO (justinfagnani): handle absolute vs relative paths separately.\n\n const location = parseLocation(path);\n const {pathname} = location;\n\n // Last-goto-wins, per controller. The navigation signal alone is not\n // enough: a child controller mounts as a *result* of its parent's render,\n // so its first goto() comes from `_onRoutesConnected` — after the parent's\n // navigation has already finished, and therefore with a signal that will\n // never abort. Without this counter a slow first child load commits over a\n // newer one. This also keeps `Routes` correct when used on its own, with\n // no `Router` and no Navigation API in the picture.\n const seq = ++this._gotoSeq;\n let tail: string | undefined;\n\n if (this.routes.length === 0 && this.fallback === undefined) {\n // If a routes controller has none of its own routes it acts like it has\n // one route of `/*` so that it passes the whole pathname as a tail\n // match.\n tail = pathname;\n this._currentPathname = '';\n // Simulate a tail group with the whole pathname\n this._currentParams = {0: tail};\n } else {\n const match = this._match(location);\n if (match === undefined) {\n throw new Error(`No route found for ${path}`);\n }\n const {route, params} = match;\n tail = match.tail;\n if (typeof route.enter === 'function') {\n const success = await route.enter(params);\n // If enter() returns false, cancel this navigation\n if (success === false) {\n return;\n }\n }\n // A newer navigation superseded this one while `enter` was awaiting.\n // Committing now would swap the outlet onto a route the URL has left.\n if (options?.signal?.aborted === true || seq !== this._gotoSeq) {\n return;\n }\n // Only update route state if the enter handler completes successfully\n this._currentRoute = route;\n this._currentParams = params;\n this._currentPathname =\n tail === undefined\n ? pathname\n : pathname.substring(0, pathname.length - tail.length);\n }\n this._currentTail = tail;\n this._currentSearch = location.search;\n this._currentHash = location.hash;\n\n // Propagate the tail match to children — deliberately NOT awaited.\n //\n // Awaiting looks like it would make `navigation.finished` cover the whole\n // tree, and an earlier revision of this fork did it. It is wrong twice\n // over. At this point `requestUpdate()` has not run, so `_childRoutes`\n // still holds the *outgoing* branch's controller: awaiting it gates the\n // parent's outlet swap on an `enter()` for a tail that controller will\n // never render (a hung one blocks the navigation forever), and if that\n // child has no route for the new tail its `No route found` throw\n // propagates out of here and `requestUpdate()` below never runs — URL\n // committed, outlet stranded, i.e. this fork's own thesis bug one level\n // down. Nested supersession is handled by the goto counter above, not by\n // awaiting. `_routeChild` covers the per-child filtering and error policy.\n //\n // Runs whether or not there is a tail. A route without one has nothing for\n // the children to render, but they must still be superseded — otherwise a\n // child mid-`enter()` for the previous tail stays current and commits over\n // a URL that has moved on.\n for (const childRoutes of this._childRoutes) {\n this._routeChild(childRoutes, tail);\n }\n this._host.requestUpdate();\n }\n\n /**\n * The result of calling the current route's render() callback.\n */\n outlet() {\n return this._currentRoute?.render?.(this._currentParams);\n }\n\n /**\n * The current parsed route parameters.\n */\n get params() {\n return this._currentParams;\n }\n\n /**\n * Hands a tail match to a child controller. Shared by the propagation loop in\n * `goto()` and the late-mount path in `_onRoutesConnected`, so that identical\n * input cannot be silent on one and an uncaught global throw on the other.\n *\n * A child with no route for the new tail is the expected case, not an error —\n * the outgoing branch mid-swap, or a deep link to a path the child cannot\n * render. Filtered structurally rather than by swallowing every rejection, so\n * a genuine `enter()` rejection still surfaces the way it does upstream.\n * Skipping must still supersede: `goto()` is where the counter is bumped, so\n * returning without it would leave an in-flight child navigation current,\n * free to commit over a URL that has moved on. A parent route with no tail\n * at all is the same case: nothing to route, but still something to stand\n * down.\n *\n * No abort signal is threaded through, and the goto is deliberately not\n * awaited. The parent commits its own state before children run, so a child\n * handed an already-aborted signal stands down with no newer goto() arriving\n * to correct it, leaving the nested outlet stuck — reachable, because a\n * hash-only navigation aborts the outstanding one without producing a\n * replacement. Supersession is the counter's job.\n */\n private _routeChild(child: Routes, tail: string | undefined) {\n if (tail === undefined) {\n child._supersede();\n return;\n }\n const childPath = formatLocation(tail, {\n search: this._currentSearch,\n hash: this._currentHash,\n });\n if (!child.hasRouteFor(childPath)) {\n child._supersede();\n return;\n }\n void child.goto(childPath).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n }\n\n /**\n * Invalidate any in-flight `goto()` on this controller without starting a\n * new one. Same-class access, so `_gotoSeq` stays private to `Routes`.\n */\n private _supersede(seen: Set<Routes> = new Set()): void {\n // Unreachable defence in depth. Upstream *can* produce a `_childRoutes`\n // cycle — a host carrying two Routes controllers, disconnected and\n // reconnected, ends up with each registered as the other's child — but\n // `hostDisconnected` below removes the listener that causes it, and a test\n // asserts the cycle cannot form. Kept because an unguarded recursive walk\n // over a cycle is a stack overflow rather than a misrender.\n if (seen.has(this)) {\n return;\n }\n seen.add(this);\n this._gotoSeq++;\n // Recursive: on the navigating branch the child's own propagation loop\n // reaches the grandchildren, but a skipped child never runs one — so\n // without this an in-flight grandchild `enter()` stays current and commits\n // over a URL that has moved on, the same defect one level deeper.\n for (const child of this._childRoutes) {\n child._supersede(seen);\n }\n }\n\n /**\n * True when this controller, or any controller below it, has a route that\n * constrains the hash.\n *\n * `Router` gates interception of fragment-only navigation on this. Left\n * ungated, a pathname-only app would have every in-page anchor swallowed and\n * re-rendered instead of scrolled; gated, such an app behaves exactly as it\n * did before hash routes existed.\n *\n * Walks children because a nested controller may route on the hash while the\n * top-level `Router` does not — and only the top-level one sees the\n * navigate event.\n */\n protected _constrainsHash(seen: Set<Routes> = new Set()): boolean {\n // Cycle guard, matching `_supersede`; see the note there.\n if (seen.has(this)) {\n return false;\n }\n seen.add(this);\n return (\n this.routes.some((r) => getPattern(r).hash !== '*') ||\n this._childRoutes.some((c) => c._constrainsHash(seen))\n );\n }\n\n /**\n * True when this controller can render `path` — i.e. a route matches, or\n * a fallback is configured.\n *\n * `Router` gates interception on this: intercepting a path we cannot render\n * commits the URL and then throws out of `goto()`, leaving the address bar\n * moved and the outlet stale. Letting the browser handle it instead means a\n * server-rendered page, an export endpoint, or a GET form still works.\n */\n hasRouteFor(path: string): boolean {\n // A fallback matches everything, and a controller with no routes of its own\n // behaves as if it had a single `/*` route (goto()'s special case). Either\n // way the answer is yes without running a single pattern — worth\n // short-circuiting, since `Router` asks this on every navigation.\n if (this.fallback !== undefined || this.routes.length === 0) {\n return true;\n }\n // `test()`, not `_match()`: this only needs the yes/no, and `exec()` pays\n // ~8x on a hit to build a groups object the caller would throw away.\n const location = parseLocation(path);\n return this.routes.some((r) => getPattern(r).test(location));\n }\n\n /**\n * Matches `pathname` against the installed routes and returns the first match\n * with its parsed parameters, or the fallback's match if one is configured.\n *\n * One `exec()` per candidate rather than `test()` to select and `exec()` to\n * extract: that ran the winning pattern twice, and every caller that wants a\n * route wants its params too.\n */\n private _match(location: RouteLocation):\n | {\n route: RouteConfig;\n params: {[key: string]: string | undefined};\n tail: string | undefined;\n }\n | undefined {\n for (const route of this.routes) {\n const result = getPattern(route).exec(location);\n if (result !== null) {\n const params: {[key: string]: string | undefined} = {\n ...result.pathname.groups,\n };\n // Named groups only. A positional group is keyed by index in every\n // component independently, so `/child/*` with a hash of `*` yields a\n // \"0\" in both — and `tailOf` picks the tail by highest numeric key.\n // Merging them would let a fragment masquerade as the tail.\n for (const groups of [result.search.groups, result.hash.groups]) {\n for (const [key, value] of Object.entries(groups)) {\n if (!/^\\d+$/.test(key)) {\n params[key] = value;\n }\n }\n }\n return {route, params, tail: tailOf(route, result.pathname.groups)};\n }\n }\n if (this.fallback === undefined) {\n return undefined;\n }\n // The fallback route behaves like it has a \"/*\" path. This is hidden from\n // the public API; the `path` is there to return a valid RouteConfig. The\n // match itself is done by hand rather than with a real `/*` pattern: a\n // nested controller is handed its tail *without* a leading slash, which\n // `/*` does not match, so a nested fallback matched nothing — empty\n // params, no tail, and its own children never routed.\n const {pathname} = location;\n const tail = pathname.startsWith('/') ? pathname.slice(1) : pathname;\n return {route: {...this.fallback, path: '/*'}, params: {0: tail}, tail};\n }\n\n hostConnected() {\n this._host.addEventListener(\n RoutesConnectedEvent.eventName,\n this._onRoutesConnected\n );\n const event = new RoutesConnectedEvent(this);\n this._host.dispatchEvent(event);\n this._onDisconnect = event.onDisconnect;\n }\n\n hostDisconnected() {\n // Remove the listener hostConnected added. Without this a host that is\n // disconnected and reconnected (a repeat() reorder, a tab swap) leaves the\n // sibling controller's listener installed, so on the second connect it\n // claims the re-dispatching controller as *its* child and the pair point\n // at each other — a real `_childRoutes` cycle, which recursive walks turn\n // into a stack overflow.\n this._host.removeEventListener(\n RoutesConnectedEvent.eventName,\n this._onRoutesConnected\n );\n // When this child routes controller is disconnected because a parent\n // outlet rendered a different template, disconnecting will ensure that\n // this controller doesn't receive a tail match meant for another route.\n this._onDisconnect?.();\n this._parentRoutes = undefined;\n }\n\n private _onRoutesConnected = (e: RoutesConnectedEvent) => {\n // Don't handle the event fired by this routes controller, which we get\n // because we do this.dispatchEvent(...)\n if (e.routes === this) {\n return;\n }\n\n const childRoutes = e.routes;\n this._childRoutes.push(childRoutes);\n childRoutes._parentRoutes = this;\n\n e.stopImmediatePropagation();\n e.onDisconnect = () => {\n const index = this._childRoutes.indexOf(childRoutes);\n if (index !== -1) {\n this._childRoutes.splice(index, 1);\n }\n };\n\n // A child that mounts under an existing tail match has to be caught up to\n // it — it missed the propagation loop in goto() that ran before it existed.\n // With no tail there is nothing to catch up to, and `_routeChild` then only\n // supersedes, a no-op on a freshly mounted child.\n this._routeChild(childRoutes, this._currentTail);\n };\n}\n\n/**\n * This event is fired from Routes controllers when their host is connected to\n * announce the child route and potentially connect to a parent routes controller.\n */\nexport class RoutesConnectedEvent extends Event {\n static readonly eventName = 'lit-routes-connected';\n readonly routes: Routes;\n onDisconnect?: () => void;\n\n constructor(routes: Routes) {\n super(RoutesConnectedEvent.eventName, {\n bubbles: true,\n composed: true,\n cancelable: false,\n });\n this.routes = routes;\n }\n}\n\ndeclare global {\n interface HTMLElementEventMap {\n [RoutesConnectedEvent.eventName]: RoutesConnectedEvent;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lit-navigation-router",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "A router for Lit built on the Navigation API. Fork of @lit-labs/router.",
5
5
  "license": "BSD-3-Clause",
6
6
  "repository": {
package/src/router.ts CHANGED
@@ -48,6 +48,15 @@ interface NavigationLike {
48
48
  const getNavigation = (): NavigationLike | undefined =>
49
49
  (window as unknown as {navigation?: NavigationLike}).navigation;
50
50
 
51
+ /**
52
+ * The current location as a path string, in the form `goto()` parses.
53
+ *
54
+ * Only the origin is dropped: this router matches within one origin, and
55
+ * `_onNavigate` declines anything else before it gets here.
56
+ */
57
+ const currentPath = (): string =>
58
+ window.location.pathname + window.location.search + window.location.hash;
59
+
51
60
  /**
52
61
  * True when the Navigation API is available — Baseline Newly Available since
53
62
  * January 2026 (Chrome/Edge, Safari 26.2, Firefox 147).
@@ -128,7 +137,7 @@ export class Router extends Routes {
128
137
  // Surfaced rather than left as a bare unhandled rejection, matching the
129
138
  // convention in routes.ts: on an engine without the API this is the *only*
130
139
  // rendering path, and a deep link with no matching route throws here.
131
- void this.goto(window.location.pathname).catch((err) => {
140
+ void this.goto(currentPath()).catch((err) => {
132
141
  queueMicrotask(() => {
133
142
  throw err;
134
143
  });
@@ -149,17 +158,21 @@ export class Router extends Routes {
149
158
  */
150
159
  private _onNavigate = (e: NavigateEventLike) => {
151
160
  // Not ours to handle: anything the browser says cannot be intercepted,
152
- // fragment-only moves, downloads, and POST form submissions.
161
+ // downloads, and POST form submissions.
153
162
  //
154
163
  // `!= null`, not `!== null`: the spec types both as nullable-but-present,
155
164
  // but a polyfill that leaves either unset would make a strict check true
156
165
  // for every ordinary link and silently decline the whole app.
157
- if (
158
- !e.canIntercept ||
159
- e.hashChange ||
160
- e.downloadRequest != null ||
161
- e.formData != null
162
- ) {
166
+ if (!e.canIntercept || e.downloadRequest != null || e.formData != null) {
167
+ return;
168
+ }
169
+
170
+ // Fragment-only moves belong to the browser unless a route actually reads
171
+ // the fragment. Intercepting them unconditionally would cost every
172
+ // pathname-only app its native in-page scrolling — and re-render the same
173
+ // route to no effect — while declining them unconditionally is the
174
+ // lit/lit#3517 bug this router inherited.
175
+ if (e.hashChange && !this._constrainsHash()) {
163
176
  return;
164
177
  }
165
178
 
@@ -196,7 +209,8 @@ export class Router extends Routes {
196
209
  // throws "No route found", and the address bar is left pointing somewhere
197
210
  // the outlet never went. Declining lets the browser do the real
198
211
  // navigation, which is the correct outcome.
199
- if (!this.hasRouteFor(url.pathname)) {
212
+ const path = url.pathname + url.search + url.hash;
213
+ if (!this.hasRouteFor(path)) {
200
214
  return;
201
215
  }
202
216
 
@@ -205,7 +219,7 @@ export class Router extends Routes {
205
219
  handler: async () => {
206
220
  // `e.signal` aborts if another navigation starts before this handler
207
221
  // resolves; goto() checks it after `enter()` and stands down.
208
- await this.goto(url.pathname, {signal: e.signal});
222
+ await this.goto(path, {signal: e.signal});
209
223
  },
210
224
  });
211
225
  };