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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,89 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Fixed
6
+
7
+ - **Routes can match on `search` and `hash`**
8
+ ([#13](https://github.com/VanLandinghamLabs/lit-router/issues/13), inherited
9
+ from [lit/lit#3517](https://github.com/lit/lit/issues/3517)). Patterns were
10
+ executed as `exec({pathname})`, and `URLPattern` defaults every component the
11
+ caller omits to the empty string — so `new URLPattern({hash: 'one'})` was
12
+ compared against a hash of `''` and could never match. A hash- or
13
+ search-constrained route matched nothing at all, which made `goto()` throw
14
+ `No route found` on every load rather than merely failing to fire. Matching
15
+ now sees the whole location. Named groups captured from the search and hash
16
+ are merged into `params`; positional ones are not, since each component
17
+ numbers its groups from zero and a merged `"0"` would masquerade as a tail.
18
+ - **`goto()` is handed the search and hash.** `Router` called
19
+ `goto(window.location.pathname)` and `goto(url.pathname)`, so both were
20
+ discarded before matching ran. `goto()` now accepts and parses
21
+ `path?search#hash`, and passes them down to child controllers alongside the
22
+ tail.
23
+ - **Fragment-only navigation is routed when a route asks for it.** `Router`
24
+ declined every navigation with `hashChange` set, so a hash route could not be
25
+ reached by clicking a link even once matching was fixed. Such navigations are
26
+ now intercepted when — and only when — some route in the tree constrains the
27
+ hash, so pathname-only apps keep the browser's native in-page scrolling.
28
+
29
+ ### Changed
30
+
31
+ - `URLPatternLike` requires `hash`: the pattern string, which a real
32
+ `URLPattern` exposes and which is how `Router` decides whether a
33
+ fragment-only navigation is its business. Its `test()`/`exec()` take a
34
+ `{pathname, search, hash}` input, and `exec()` returns the `search` and
35
+ `hash` groups alongside the pathname's.
36
+
37
+ ### Documentation
38
+
39
+ - The `URLPatternRouteConfig` doc comment claimed patterns were "limited to
40
+ checking `pathname` and `search`". `search` never worked either; both now do.
41
+
42
+ ## 0.4.0
43
+
44
+ ### Fixed
45
+
46
+ - **A route's tail is identified from its pattern, not guessed from the
47
+ match's groups object**
48
+ ([#4](https://github.com/VanLandinghamLabs/lit-router/issues/4)).
49
+ `URLPattern` keys every unnamed group by position, so an unnamed regex group
50
+ (`/post/(\d+)`) and a wildcard that is not last (`/foo/*/bar`) looked exactly
51
+ like a trailing `/*`. Both were handed to child controllers as a tail and
52
+ stripped from `link()`, which returned `/post/` for `/post/123` and a
53
+ truncated `/foo/zz/b` for `/foo/zz/bar`. Only a pattern that ends in a
54
+ wildcard now has a tail.
55
+ - **A nested `fallback` passes its tail on to its own children**
56
+ ([#5](https://github.com/VanLandinghamLabs/lit-router/issues/5)). The
57
+ fallback matched with a literal `/*` pattern, but a nested controller is
58
+ handed its tail without a leading slash, which `/*` rejects: the fallback
59
+ rendered with empty params and grandchildren were never routed or
60
+ superseded. It now behaves like `/*` at the root and `*` when nested, with
61
+ `params[0]` the whole tail in both cases.
62
+ - **Children are superseded when the parent moves to a route with no tail**
63
+ ([#7](https://github.com/VanLandinghamLabs/lit-router/issues/7)). The
64
+ propagation loop ran only when the new route had a tail, so a child
65
+ mid-`enter()` for the previous tail was never stood down and could commit
66
+ over a URL that had already moved on.
67
+
68
+ ### Changed
69
+
70
+ - `URLPatternLike` requires `pathname`: the pattern string, which a real
71
+ `URLPattern` exposes and which is how the tail is now identified. An object
72
+ offering only `test()`/`exec()` no longer type-checks as a route pattern.
73
+ - A trailing `*` counts as a tail only when it is a wildcard: bare `*`, `{*}`,
74
+ `(.*)` (which `URLPattern` normalises to `*`), optionally followed by `?`.
75
+ A `*` that is the modifier on a group or a named param (`(\d+)*`, `{/}*`,
76
+ `:rest*`), or an escaped `\*`, is not. Previously any pattern whose match
77
+ produced a positional group was treated as having a tail.
78
+
79
+ ### Documentation
80
+
81
+ - A nested index route is spelled `{path: ''}`
82
+ ([#6](https://github.com/VanLandinghamLabs/lit-router/issues/6)). The tail
83
+ handed to a child has no leading slash, so the index of a nested route space
84
+ is the empty string; `{path: '/'}` matches nothing there. This already
85
+ worked and is now documented and pinned by a test.
86
+
3
87
  ## 0.3.0
4
88
 
5
89
  ### Fixed
package/NOTICE.md CHANGED
@@ -105,6 +105,20 @@ it is now answered:
105
105
  Nested supersession is the counter's job, not the await's.
106
106
  - New `hasRouteFor(pathname)`, used by `Router` to decline what it cannot
107
107
  render.
108
+ - **The tail is identified from the pattern, not from the match.** Upstream
109
+ takes the highest positional group of any match as the tail, but `URLPattern`
110
+ keys an unnamed regex group (`/post/(\d+)`) and a non-final wildcard
111
+ (`/foo/*/bar`) by position too, so both were handed to children and stripped
112
+ from `link()`. `tailOf()` reads the compiled pattern's `pathname` and only
113
+ looks for a tail when it ends in a wildcard; `URLPatternLike.pathname` is
114
+ therefore required.
115
+ - The fallback matches by hand rather than with a `/*` pattern. A nested
116
+ controller is handed its tail without a leading slash, which `/*` rejects, so
117
+ upstream's nested fallback saw empty params and never routed its own
118
+ children.
119
+ - Children are handed to `_routeChild` on every parent navigation, tail or
120
+ not: a route with no tail still supersedes them, so a child mid-`enter()` for
121
+ the previous tail cannot commit over a URL that has moved on.
108
122
 
109
123
  ### Known limits
110
124
 
@@ -120,9 +134,10 @@ it is now answered:
120
134
  - The `seen` set in `_supersede()` is defence in depth and unpinned **by
121
135
  construction**: since `hostDisconnected` removes its listener, no test can
122
136
  build a `_childRoutes` cycle any more.
123
- - A child skipped because it cannot render the new tail keeps its previously
124
- *committed* outlet — it is superseded (no in-flight navigation can commit)
125
- but not cleared, so it goes on rendering the route the URL has left. Upstream
137
+ - A child skipped because it cannot render the new tail — or because the
138
+ parent's new route has no tail at all — keeps its previously *committed*
139
+ outlet: it is superseded (no in-flight navigation can commit) but not
140
+ cleared, so it goes on rendering the route the URL has left. Upstream
126
141
  had the same end state by a different route (`No route found` threw before
127
142
  any state changed). Clearing it needs `_currentRoute` reset plus a host
128
143
  update, which is a behaviour change rather than a bug fix.
@@ -140,7 +155,8 @@ Runner so it stands alone. Test changes:
140
155
  - Two `(r: RouteConfig)` annotations added in `router_test.ts` (upstream relied
141
156
  on monorepo-wide inference). **Otherwise upstream's 6 tests are unmodified and
142
157
  pass**, which is the main evidence that the rewrite preserves behaviour.
143
- - 14 new tests in `src/test/navigation_test.ts` cover the Navigation API path.
158
+ - 26 new tests in `src/test/navigation_test.ts` cover the Navigation API path
159
+ and the nested-routing fixes above.
144
160
  The first asserts the suite is actually running against `window.navigation`
145
161
  rather than silently falling back — without it the rest would pass against the
146
162
  legacy path and prove nothing. It is kept as a guard for the day this
@@ -148,7 +164,7 @@ Runner so it stands alone. Test changes:
148
164
 
149
165
  ## Verified
150
166
 
151
- `npm test` → 20 passed (6 upstream + 14 new), Chromium via Playwright.
167
+ `npm test` → 32 passed (6 upstream + 26 new), Chromium via Playwright.
152
168
 
153
169
  Run `npm run clean` before a mutation check: the build is `composite`/
154
170
  `incremental`, and a stale `development/` can contain a hunk's comment without
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # lit-navigation-router
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/lit-navigation-router)](https://www.npmjs.com/package/lit-navigation-router)
4
+
3
5
  A router for Lit, built on the [Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API).
6
+ Published on npm as [`lit-navigation-router`](https://www.npmjs.com/package/lit-navigation-router).
4
7
 
5
8
  Fork of [`@lit-labs/router`](https://github.com/lit/lit/tree/main/packages/labs/router)
6
9
  — see [NOTICE.md](./NOTICE.md) for provenance, licence and the full list of
@@ -57,6 +60,59 @@ this._router.interceptOptions = {scroll: 'manual', focusReset: 'manual'};
57
60
  await routes.goto('/item/1', {signal});
58
61
  ```
59
62
 
63
+ ## Nested routes and tails
64
+
65
+ A route whose pattern ends in a wildcard — `/docs/*` — hands what the wildcard
66
+ matched (the *tail*) to any `Routes` controller mounted by its `render()`. The
67
+ tail has no leading slash, and child routes are written the same way:
68
+
69
+ ```ts
70
+ // Parent
71
+ {path: '/docs/*', render: () => html`<my-docs></my-docs>`}
72
+
73
+ // Child, inside <my-docs>
74
+ private _routes = new Routes(this, [
75
+ {path: '', render: () => html`<h2>Docs</h2>`}, // /docs/
76
+ {path: ':page', render: ({page}) => html`<doc-page .page=${page}></doc-page>`}, // /docs/intro
77
+ ]);
78
+ ```
79
+
80
+ The index of a nested route space is the empty tail, spelled `{path: ''}`.
81
+ `{path: '/'}` matches nothing there: it would need a tail of `/`, i.e. a URL of
82
+ `/docs//`.
83
+
84
+ Only a trailing wildcard produces a tail. An unnamed regex group
85
+ (`/post/(\d+)`) or a wildcard followed by more pattern (`/a/*/b`) is a
86
+ parameter of that route, available as `params[0]`; it is neither passed to
87
+ children nor stripped from `link()`. A `fallback` behaves like a `/*` route and
88
+ passes the whole pathname on as the tail. Nested, it accepts the slash-less
89
+ tail it is handed and passes that on.
90
+
91
+ ## Search and hash routes
92
+
93
+ A `pattern` route can constrain `search` and `hash`, not just `pathname`:
94
+
95
+ ```ts
96
+ {pattern: new URLPattern({pathname: '/docs', hash: ':section'}),
97
+ render: ({section}) => html`<doc-page .section=${section}></doc-page>`}
98
+
99
+ {pattern: new URLPattern({pathname: '/search', search: 'q=:term'}),
100
+ render: ({term}) => html`<results-for .term=${term}></results-for>`}
101
+ ```
102
+
103
+ Named groups captured from the search and hash join the pathname's in `params`.
104
+ Positional ones do not: every component numbers its groups from zero
105
+ independently, so merging them would make a fragment indistinguishable from a
106
+ tail. Name the groups you need.
107
+
108
+ Only the pathname nests. A child controller is handed the parent's tail as its
109
+ pathname and the *same* search and hash — there is no way to split a query
110
+ string or a fragment across a route tree.
111
+
112
+ Fragment-only navigation (`<a href="#section">`) is intercepted **only** when
113
+ some route constrains the hash. An app that routes on pathnames alone keeps the
114
+ browser's native in-page scrolling.
115
+
60
116
  ## Browser support — please read
61
117
 
62
118
  This router **requires the Navigation API**. There is no legacy fallback.
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAqBnC,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAC;IAC3C,MAAM,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAC;CACxC;AAgBD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,QAAO,OAEgB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,qBAAa,MAAO,SAAQ,MAAM;IAChC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAEpC,OAAO,CAAC,UAAU,CAAS;IAElB,aAAa;IAwBb,gBAAgB;IAQzB;;;OAGG;IACH,OAAO,CAAC,WAAW,CA6DjB;CACH"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAqBnC,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAC;IAC3C,MAAM,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAC;CACxC;AAyBD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,QAAO,OAEgB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,qBAAa,MAAO,SAAQ,MAAM;IAChC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAEpC,OAAO,CAAC,UAAU,CAAS;IAElB,aAAa;IAwBb,gBAAgB;IAQzB;;;OAGG;IACH,OAAO,CAAC,WAAW,CAkEjB;CACH"}
@@ -8,6 +8,13 @@
8
8
  */
9
9
  import { Routes } from './routes.js';
10
10
  const getNavigation = () => window.navigation;
11
+ /**
12
+ * The current location as a path string, in the form `goto()` parses.
13
+ *
14
+ * Only the origin is dropped: this router matches within one origin, and
15
+ * `_onNavigate` declines anything else before it gets here.
16
+ */
17
+ const currentPath = () => window.location.pathname + window.location.search + window.location.hash;
11
18
  /**
12
19
  * True when the Navigation API is available — Baseline Newly Available since
13
20
  * January 2026 (Chrome/Edge, Safari 26.2, Firefox 147).
@@ -84,7 +91,7 @@ export class Router extends Routes {
84
91
  // Surfaced rather than left as a bare unhandled rejection, matching the
85
92
  // convention in routes.ts: on an engine without the API this is the *only*
86
93
  // rendering path, and a deep link with no matching route throws here.
87
- void this.goto(window.location.pathname).catch((err) => {
94
+ void this.goto(currentPath()).catch((err) => {
88
95
  queueMicrotask(() => {
89
96
  throw err;
90
97
  });
@@ -103,15 +110,20 @@ export class Router extends Routes {
103
110
  */
104
111
  _onNavigate = (e) => {
105
112
  // Not ours to handle: anything the browser says cannot be intercepted,
106
- // fragment-only moves, downloads, and POST form submissions.
113
+ // downloads, and POST form submissions.
107
114
  //
108
115
  // `!= null`, not `!== null`: the spec types both as nullable-but-present,
109
116
  // but a polyfill that leaves either unset would make a strict check true
110
117
  // for every ordinary link and silently decline the whole app.
111
- if (!e.canIntercept ||
112
- e.hashChange ||
113
- e.downloadRequest != null ||
114
- e.formData != null) {
118
+ if (!e.canIntercept || e.downloadRequest != null || e.formData != null) {
119
+ return;
120
+ }
121
+ // Fragment-only moves belong to the browser unless a route actually reads
122
+ // the fragment. Intercepting them unconditionally would cost every
123
+ // pathname-only app its native in-page scrolling — and re-render the same
124
+ // route to no effect — while declining them unconditionally is the
125
+ // lit/lit#3517 bug this router inherited.
126
+ if (e.hashChange && !this._constrainsHash()) {
115
127
  return;
116
128
  }
117
129
  // Reloads must stay reloads. `canIntercept` is true for them, so without
@@ -144,7 +156,8 @@ export class Router extends Routes {
144
156
  // throws "No route found", and the address bar is left pointing somewhere
145
157
  // the outlet never went. Declining lets the browser do the real
146
158
  // navigation, which is the correct outcome.
147
- if (!this.hasRouteFor(url.pathname)) {
159
+ const path = url.pathname + url.search + url.hash;
160
+ if (!this.hasRouteFor(path)) {
148
161
  return;
149
162
  }
150
163
  e.intercept({
@@ -152,7 +165,7 @@ export class Router extends Routes {
152
165
  handler: async () => {
153
166
  // `e.signal` aborts if another navigation starts before this handler
154
167
  // resolves; goto() checks it after `enter()` and stands down.
155
- await this.goto(url.pathname, { signal: e.signal });
168
+ await this.goto(path, { signal: e.signal });
156
169
  },
157
170
  });
158
171
  };
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.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.\n * Rebuilt on the Navigation API; see NOTICE.md.\n */\n\nimport {Routes} from './routes.js';\n\n/**\n * The slice of `NavigateEvent` this router reads. Declared locally rather than\n * typing the handler `any`: these properties *are* the correctness boundary, so\n * a typo like `hashchange` for `hashChange` would silently disable a filter\n * forever. Names verified against Chromium's `NavigateEvent.prototype`.\n */\ninterface NavigateEventLike {\n readonly canIntercept: boolean;\n readonly hashChange: boolean;\n readonly downloadRequest: string | null;\n readonly formData: FormData | null;\n readonly navigationType: 'push' | 'replace' | 'reload' | 'traverse';\n readonly signal: AbortSignal;\n readonly destination: {readonly url: string};\n /** Not in every engine yet; used only as a best-effort `rel` check. */\n readonly sourceElement?: Element | null;\n intercept(options: InterceptOptions & {handler?: () => Promise<void>}): void;\n}\n\n/** The subset of `NavigationInterceptOptions` this router forwards. */\nexport interface InterceptOptions {\n focusReset?: 'after-transition' | 'manual';\n scroll?: 'after-transition' | 'manual';\n}\n\ninterface NavigationLike {\n addEventListener(\n type: 'navigate',\n listener: (e: NavigateEventLike) => void\n ): void;\n removeEventListener(\n type: 'navigate',\n listener: (e: NavigateEventLike) => void\n ): void;\n}\n\nconst getNavigation = (): NavigationLike | undefined =>\n (window as unknown as {navigation?: NavigationLike}).navigation;\n\n/**\n * True when the Navigation API is available — Baseline Newly Available since\n * January 2026 (Chrome/Edge, Safari 26.2, Firefox 147).\n *\n * This router **requires** it. Exported so an app can detect an unsupported\n * engine at boot and say so, rather than leaving the user to notice that every\n * link reloads the page.\n */\nexport const supportsNavigationApi = (): boolean =>\n typeof window !== 'undefined' &&\n typeof getNavigation()?.addEventListener === 'function';\n\n/**\n * A root-level router that intercepts navigation via the Navigation API.\n *\n * This class extends Routes so that it can also have a route configuration.\n *\n * There should only be one Router instance on a page, since the Router\n * installs a global listener. Nested routes should be configured with the\n * `Routes` class.\n *\n * ## Why the Navigation API\n *\n * Upstream intercepted navigation with a global click listener plus `popstate`\n * and committed with `history.pushState()`. That is structurally racy:\n * `pushState` is synchronous and `popstate` fires *after* the URL has already\n * moved, but `goto()` awaits `route.enter()` before swapping the outlet. The\n * URL leads and the outlet lags, leaving two sources of truth — the outgoing\n * route re-renders with stale params, and two quick navigations commit in\n * whatever order their `enter()` hooks happen to resolve.\n *\n * `navigateEvent.intercept({handler})` collapses that. The browser commits the\n * URL and holds the navigation un-finished while the handler runs, and it\n * aborts `navigateEvent.signal` when a newer navigation supersedes this one —\n * which `goto()` honours, so a superseded route can no longer win the outlet.\n *\n * ## No legacy fallback\n *\n * An earlier version of this fork kept upstream's click/popstate path for\n * pre-2026 engines. It was removed deliberately. Ten review rounds found\n * divergences between the two paths and **every one was in the click handler**,\n * never in this one — which is structural, not luck: this handler reads a\n * decision the browser has already made, while the click handler had to\n * re-derive it, re-implementing the rules for choosing a navigable, the\n * fragment-navigation predicate, and the modifier-key rules. Each round found\n * another place where the re-implementation and the spec disagreed.\n *\n * On an engine without the API, links fall back to ordinary full page loads.\n * For an app whose server serves the shell on every route that still works —\n * it is slower, not broken — and `supportsNavigationApi()` lets you detect it.\n * If real pre-2026 support is ever needed, use a Navigation API polyfill: one\n * decision path, with compatibility isolated in a layer whose whole job is\n * spec accuracy.\n */\nexport class Router extends Routes {\n /**\n * Options forwarded to `navigateEvent.intercept()`. Leaving these unset\n * gives the browser's default scroll and focus handling.\n */\n interceptOptions?: InterceptOptions;\n\n private _listening = false;\n\n override hostConnected() {\n super.hostConnected();\n // Gated on the exported predicate, not on `navigation !== undefined`:\n // a stub or partial polyfill under that name would otherwise make this\n // branch throw out of connectedCallback while `supportsNavigationApi()`\n // told the app it was unsupported — and then even the initial render below\n // would not run.\n if (supportsNavigationApi()) {\n getNavigation()!.addEventListener('navigate', this._onNavigate);\n this._listening = true;\n }\n // Kick off routed rendering by going to the current URL. Done even without\n // the API: a full page load still renders the right route, which is what\n // makes the unsupported-engine degradation \"slow\" rather than \"blank\".\n // Surfaced rather than left as a bare unhandled rejection, matching the\n // convention in routes.ts: on an engine without the API this is the *only*\n // rendering path, and a deep link with no matching route throws here.\n void this.goto(window.location.pathname).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n }\n\n override hostDisconnected() {\n super.hostDisconnected();\n if (this._listening) {\n getNavigation()?.removeEventListener('navigate', this._onNavigate);\n this._listening = false;\n }\n }\n\n /**\n * Handles same-document navigation from every source at once: anchor clicks,\n * `navigation.navigate()`, `history.pushState()`, and back/forward.\n */\n private _onNavigate = (e: NavigateEventLike) => {\n // Not ours to handle: anything the browser says cannot be intercepted,\n // fragment-only moves, downloads, and POST form submissions.\n //\n // `!= null`, not `!== null`: the spec types both as nullable-but-present,\n // but a polyfill that leaves either unset would make a strict check true\n // for every ordinary link and silently decline the whole app.\n if (\n !e.canIntercept ||\n e.hashChange ||\n e.downloadRequest != null ||\n e.formData != null\n ) {\n return;\n }\n\n // Reloads must stay reloads. `canIntercept` is true for them, so without\n // this `location.reload()` silently degrades to re-running goto() on the\n // same path — the document is never replaced, breaking the standard\n // \"new version available, reload\" escape hatch. (It would also disagree\n // with the browser's own refresh button, which is not interceptable.)\n if (e.navigationType === 'reload') {\n return;\n }\n\n // `rel=\"external\"` is a convention this router honours — it is not defined\n // by HTML or by the Navigation API, so the browser will not decline these\n // for us. Best-effort: `sourceElement` is not in every engine, and is\n // absent for programmatic navigation.\n if (e.sourceElement?.getAttribute?.('rel') === 'external') {\n return;\n }\n\n // Read per navigation rather than cached at module scope: the value cannot\n // change, but reading it on import makes merely importing this module throw\n // where there is no `location` (SSR, a bundler evaluating for tree-shaking\n // under the package's `sideEffects: false` claim).\n const url = new URL(e.destination.url);\n if (url.origin !== window.location.origin) {\n return;\n }\n\n // Only intercept what we can actually render. `canIntercept` is true for\n // any same-origin URL, including cross-document ones — so without this a\n // link to a server-rendered page, an export endpoint, or a GET form\n // (whose `formData` is null) gets swallowed: the URL commits, goto()\n // throws \"No route found\", and the address bar is left pointing somewhere\n // the outlet never went. Declining lets the browser do the real\n // navigation, which is the correct outcome.\n if (!this.hasRouteFor(url.pathname)) {\n return;\n }\n\n e.intercept({\n ...this.interceptOptions,\n handler: async () => {\n // `e.signal` aborts if another navigation starts before this handler\n // resolves; goto() checks it after `enter()` and stands down.\n await this.goto(url.pathname, {signal: e.signal});\n },\n });\n };\n}\n"]}
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAsCnC,MAAM,aAAa,GAAG,GAA+B,EAAE,CACpD,MAAmD,CAAC,UAAU,CAAC;AAElE;;;;;GAKG;AACH,MAAM,WAAW,GAAG,GAAW,EAAE,CAC/B,MAAM,CAAC,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;AAE3E;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAY,EAAE,CACjD,OAAO,MAAM,KAAK,WAAW;IAC7B,OAAO,aAAa,EAAE,EAAE,gBAAgB,KAAK,UAAU,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAM,OAAO,MAAO,SAAQ,MAAM;IAChC;;;OAGG;IACH,gBAAgB,CAAoB;IAE5B,UAAU,GAAG,KAAK,CAAC;IAElB,aAAa;QACpB,KAAK,CAAC,aAAa,EAAE,CAAC;QACtB,sEAAsE;QACtE,uEAAuE;QACvE,wEAAwE;QACxE,2EAA2E;QAC3E,iBAAiB;QACjB,IAAI,qBAAqB,EAAE,EAAE,CAAC;YAC5B,aAAa,EAAG,CAAC,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YAChE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACzB,CAAC;QACD,2EAA2E;QAC3E,yEAAyE;QACzE,uEAAuE;QACvE,wEAAwE;QACxE,2EAA2E;QAC3E,sEAAsE;QACtE,KAAK,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YAC1C,cAAc,CAAC,GAAG,EAAE;gBAClB,MAAM,GAAG,CAAC;YACZ,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;IAEQ,gBAAgB;QACvB,KAAK,CAAC,gBAAgB,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,aAAa,EAAE,EAAE,mBAAmB,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YACnE,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QAC1B,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,WAAW,GAAG,CAAC,CAAoB,EAAE,EAAE;QAC7C,uEAAuE;QACvE,wCAAwC;QACxC,EAAE;QACF,0EAA0E;QAC1E,yEAAyE;QACzE,8DAA8D;QAC9D,IAAI,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,eAAe,IAAI,IAAI,IAAI,CAAC,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;YACvE,OAAO;QACT,CAAC;QAED,0EAA0E;QAC1E,mEAAmE;QACnE,0EAA0E;QAC1E,mEAAmE;QACnE,0CAA0C;QAC1C,IAAI,CAAC,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,CAAC;YAC5C,OAAO;QACT,CAAC;QAED,yEAAyE;QACzE,yEAAyE;QACzE,oEAAoE;QACpE,wEAAwE;QACxE,sEAAsE;QACtE,IAAI,CAAC,CAAC,cAAc,KAAK,QAAQ,EAAE,CAAC;YAClC,OAAO;QACT,CAAC;QAED,2EAA2E;QAC3E,0EAA0E;QAC1E,sEAAsE;QACtE,sCAAsC;QACtC,IAAI,CAAC,CAAC,aAAa,EAAE,YAAY,EAAE,CAAC,KAAK,CAAC,KAAK,UAAU,EAAE,CAAC;YAC1D,OAAO;QACT,CAAC;QAED,2EAA2E;QAC3E,4EAA4E;QAC5E,2EAA2E;QAC3E,mDAAmD;QACnD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;YAC1C,OAAO;QACT,CAAC;QAED,yEAAyE;QACzE,yEAAyE;QACzE,oEAAoE;QACpE,qEAAqE;QACrE,0EAA0E;QAC1E,gEAAgE;QAChE,4CAA4C;QAC5C,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC;QAClD,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,CAAC,CAAC,SAAS,CAAC;YACV,GAAG,IAAI,CAAC,gBAAgB;YACxB,OAAO,EAAE,KAAK,IAAI,EAAE;gBAClB,qEAAqE;gBACrE,8DAA8D;gBAC9D,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAC,CAAC,CAAC;YAC5C,CAAC;SACF,CAAC,CAAC;IACL,CAAC,CAAC;CACH","sourcesContent":["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Modifications Copyright 2026 VanLandingham Labs, same license.\n * Rebuilt on the Navigation API; see NOTICE.md.\n */\n\nimport {Routes} from './routes.js';\n\n/**\n * The slice of `NavigateEvent` this router reads. Declared locally rather than\n * typing the handler `any`: these properties *are* the correctness boundary, so\n * a typo like `hashchange` for `hashChange` would silently disable a filter\n * forever. Names verified against Chromium's `NavigateEvent.prototype`.\n */\ninterface NavigateEventLike {\n readonly canIntercept: boolean;\n readonly hashChange: boolean;\n readonly downloadRequest: string | null;\n readonly formData: FormData | null;\n readonly navigationType: 'push' | 'replace' | 'reload' | 'traverse';\n readonly signal: AbortSignal;\n readonly destination: {readonly url: string};\n /** Not in every engine yet; used only as a best-effort `rel` check. */\n readonly sourceElement?: Element | null;\n intercept(options: InterceptOptions & {handler?: () => Promise<void>}): void;\n}\n\n/** The subset of `NavigationInterceptOptions` this router forwards. */\nexport interface InterceptOptions {\n focusReset?: 'after-transition' | 'manual';\n scroll?: 'after-transition' | 'manual';\n}\n\ninterface NavigationLike {\n addEventListener(\n type: 'navigate',\n listener: (e: NavigateEventLike) => void\n ): void;\n removeEventListener(\n type: 'navigate',\n listener: (e: NavigateEventLike) => void\n ): void;\n}\n\nconst getNavigation = (): NavigationLike | undefined =>\n (window as unknown as {navigation?: NavigationLike}).navigation;\n\n/**\n * The current location as a path string, in the form `goto()` parses.\n *\n * Only the origin is dropped: this router matches within one origin, and\n * `_onNavigate` declines anything else before it gets here.\n */\nconst currentPath = (): string =>\n window.location.pathname + window.location.search + window.location.hash;\n\n/**\n * True when the Navigation API is available — Baseline Newly Available since\n * January 2026 (Chrome/Edge, Safari 26.2, Firefox 147).\n *\n * This router **requires** it. Exported so an app can detect an unsupported\n * engine at boot and say so, rather than leaving the user to notice that every\n * link reloads the page.\n */\nexport const supportsNavigationApi = (): boolean =>\n typeof window !== 'undefined' &&\n typeof getNavigation()?.addEventListener === 'function';\n\n/**\n * A root-level router that intercepts navigation via the Navigation API.\n *\n * This class extends Routes so that it can also have a route configuration.\n *\n * There should only be one Router instance on a page, since the Router\n * installs a global listener. Nested routes should be configured with the\n * `Routes` class.\n *\n * ## Why the Navigation API\n *\n * Upstream intercepted navigation with a global click listener plus `popstate`\n * and committed with `history.pushState()`. That is structurally racy:\n * `pushState` is synchronous and `popstate` fires *after* the URL has already\n * moved, but `goto()` awaits `route.enter()` before swapping the outlet. The\n * URL leads and the outlet lags, leaving two sources of truth — the outgoing\n * route re-renders with stale params, and two quick navigations commit in\n * whatever order their `enter()` hooks happen to resolve.\n *\n * `navigateEvent.intercept({handler})` collapses that. The browser commits the\n * URL and holds the navigation un-finished while the handler runs, and it\n * aborts `navigateEvent.signal` when a newer navigation supersedes this one —\n * which `goto()` honours, so a superseded route can no longer win the outlet.\n *\n * ## No legacy fallback\n *\n * An earlier version of this fork kept upstream's click/popstate path for\n * pre-2026 engines. It was removed deliberately. Ten review rounds found\n * divergences between the two paths and **every one was in the click handler**,\n * never in this one — which is structural, not luck: this handler reads a\n * decision the browser has already made, while the click handler had to\n * re-derive it, re-implementing the rules for choosing a navigable, the\n * fragment-navigation predicate, and the modifier-key rules. Each round found\n * another place where the re-implementation and the spec disagreed.\n *\n * On an engine without the API, links fall back to ordinary full page loads.\n * For an app whose server serves the shell on every route that still works —\n * it is slower, not broken — and `supportsNavigationApi()` lets you detect it.\n * If real pre-2026 support is ever needed, use a Navigation API polyfill: one\n * decision path, with compatibility isolated in a layer whose whole job is\n * spec accuracy.\n */\nexport class Router extends Routes {\n /**\n * Options forwarded to `navigateEvent.intercept()`. Leaving these unset\n * gives the browser's default scroll and focus handling.\n */\n interceptOptions?: InterceptOptions;\n\n private _listening = false;\n\n override hostConnected() {\n super.hostConnected();\n // Gated on the exported predicate, not on `navigation !== undefined`:\n // a stub or partial polyfill under that name would otherwise make this\n // branch throw out of connectedCallback while `supportsNavigationApi()`\n // told the app it was unsupported — and then even the initial render below\n // would not run.\n if (supportsNavigationApi()) {\n getNavigation()!.addEventListener('navigate', this._onNavigate);\n this._listening = true;\n }\n // Kick off routed rendering by going to the current URL. Done even without\n // the API: a full page load still renders the right route, which is what\n // makes the unsupported-engine degradation \"slow\" rather than \"blank\".\n // Surfaced rather than left as a bare unhandled rejection, matching the\n // convention in routes.ts: on an engine without the API this is the *only*\n // rendering path, and a deep link with no matching route throws here.\n void this.goto(currentPath()).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n }\n\n override hostDisconnected() {\n super.hostDisconnected();\n if (this._listening) {\n getNavigation()?.removeEventListener('navigate', this._onNavigate);\n this._listening = false;\n }\n }\n\n /**\n * Handles same-document navigation from every source at once: anchor clicks,\n * `navigation.navigate()`, `history.pushState()`, and back/forward.\n */\n private _onNavigate = (e: NavigateEventLike) => {\n // Not ours to handle: anything the browser says cannot be intercepted,\n // downloads, and POST form submissions.\n //\n // `!= null`, not `!== null`: the spec types both as nullable-but-present,\n // but a polyfill that leaves either unset would make a strict check true\n // for every ordinary link and silently decline the whole app.\n if (!e.canIntercept || e.downloadRequest != null || e.formData != null) {\n return;\n }\n\n // Fragment-only moves belong to the browser unless a route actually reads\n // the fragment. Intercepting them unconditionally would cost every\n // pathname-only app its native in-page scrolling — and re-render the same\n // route to no effect — while declining them unconditionally is the\n // lit/lit#3517 bug this router inherited.\n if (e.hashChange && !this._constrainsHash()) {\n return;\n }\n\n // Reloads must stay reloads. `canIntercept` is true for them, so without\n // this `location.reload()` silently degrades to re-running goto() on the\n // same path — the document is never replaced, breaking the standard\n // \"new version available, reload\" escape hatch. (It would also disagree\n // with the browser's own refresh button, which is not interceptable.)\n if (e.navigationType === 'reload') {\n return;\n }\n\n // `rel=\"external\"` is a convention this router honours — it is not defined\n // by HTML or by the Navigation API, so the browser will not decline these\n // for us. Best-effort: `sourceElement` is not in every engine, and is\n // absent for programmatic navigation.\n if (e.sourceElement?.getAttribute?.('rel') === 'external') {\n return;\n }\n\n // Read per navigation rather than cached at module scope: the value cannot\n // change, but reading it on import makes merely importing this module throw\n // where there is no `location` (SSR, a bundler evaluating for tree-shaking\n // under the package's `sideEffects: false` claim).\n const url = new URL(e.destination.url);\n if (url.origin !== window.location.origin) {\n return;\n }\n\n // Only intercept what we can actually render. `canIntercept` is true for\n // any same-origin URL, including cross-document ones — so without this a\n // link to a server-rendered page, an export endpoint, or a GET form\n // (whose `formData` is null) gets swallowed: the URL commits, goto()\n // throws \"No route found\", and the address bar is left pointing somewhere\n // the outlet never went. Declining lets the browser do the real\n // navigation, which is the correct outcome.\n const path = url.pathname + url.search + url.hash;\n if (!this.hasRouteFor(path)) {\n return;\n }\n\n e.intercept({\n ...this.interceptOptions,\n handler: async () => {\n // `e.signal` aborts if another navigation starts before this handler\n // resolves; goto() checks it after `enter()` and stands down.\n await this.goto(path, {signal: e.signal});\n },\n });\n };\n}\n"]}
@@ -27,8 +27,8 @@ export interface PathRouteConfig extends BaseRouteConfig {
27
27
  *
28
28
  * While `URLPattern` can match against protocols, hostnames, and ports,
29
29
  * routes will only be checked for matches if they're part of the current
30
- * origin. This means that the pattern is limited to checking `pathname` and
31
- * `search`.
30
+ * origin. This means the pattern is limited to checking `pathname`, `search`
31
+ * and `hash`.
32
32
  */
33
33
  export interface URLPatternRouteConfig extends BaseRouteConfig {
34
34
  pattern: URLPatternLike;
@@ -44,19 +44,52 @@ export interface URLPatternRouteConfig extends BaseRouteConfig {
44
44
  * A real `URLPattern` satisfies this, so passing one still type-checks.
45
45
  */
46
46
  export interface URLPatternLike {
47
- test(input: {
48
- pathname: string;
49
- }): boolean;
50
- exec(input: {
51
- pathname: string;
52
- }): {
47
+ /**
48
+ * The pathname pattern string, as `URLPattern.prototype.pathname` returns
49
+ * it. Read to tell a trailing wildcard from any other positional group —
50
+ * the groups object alone cannot (see `tailOf`).
51
+ */
52
+ readonly pathname: string;
53
+ /**
54
+ * The hash pattern string. `'*'` means the route places no constraint on the
55
+ * fragment, which is what `URLPattern` fills in for any component the caller
56
+ * omits. `Router` reads this to decide whether a fragment-only navigation is
57
+ * its business or the browser's.
58
+ */
59
+ readonly hash: string;
60
+ test(input: RouteLocation): boolean;
61
+ exec(input: RouteLocation): {
53
62
  pathname: {
54
63
  groups: {
55
64
  [key: string]: string | undefined;
56
65
  };
57
66
  };
67
+ search: {
68
+ groups: {
69
+ [key: string]: string | undefined;
70
+ };
71
+ };
72
+ hash: {
73
+ groups: {
74
+ [key: string]: string | undefined;
75
+ };
76
+ };
58
77
  } | null;
59
78
  }
79
+ /**
80
+ * The parts of a location this router matches against, split out of a path
81
+ * string by `parseLocation`.
82
+ *
83
+ * `search` and `hash` carry no leading `?` or `#`. `URLPattern` canonicalises
84
+ * either form away on an init input, but only for a real `URLPattern` — the
85
+ * structural `URLPatternLike` above admits other implementations, so the
86
+ * delimiters are stripped here rather than left to the pattern.
87
+ */
88
+ export interface RouteLocation {
89
+ pathname: string;
90
+ search: string;
91
+ hash: string;
92
+ }
60
93
  /**
61
94
  * A description of a route, which path or pattern to match against, and a
62
95
  * render() callback used to render a match to the outlet.
@@ -71,7 +104,10 @@ export declare class Routes implements ReactiveController {
71
104
  routes: Array<RouteConfig>;
72
105
  /**
73
106
  * A default fallback route which will always be matched if none of the
74
- * {@link routes} match. Implicitly matches to the path "/*".
107
+ * {@link routes} match. Behaves like a `/*` route: `params[0]` is the whole
108
+ * pathname minus its leading slash, and is handed to child controllers as
109
+ * their tail. A nested controller's own pathname is a tail, with no leading
110
+ * slash; the fallback accepts that too.
75
111
  */
76
112
  fallback?: BaseRouteConfig;
77
113
  private readonly _childRoutes;
@@ -79,6 +115,16 @@ export declare class Routes implements ReactiveController {
79
115
  /** Monotonic goto counter; see the last-goto-wins note in goto(). */
80
116
  private _gotoSeq;
81
117
  private _currentPathname;
118
+ private _currentTail;
119
+ /**
120
+ * The search and hash of the current location.
121
+ *
122
+ * Ambient rather than tailed: only the pathname nests, so a child controller
123
+ * is handed the parent's tail as its pathname but the *same* search and
124
+ * hash. There is no meaningful way to split a fragment across a route tree.
125
+ */
126
+ private _currentSearch;
127
+ private _currentHash;
82
128
  private _currentRoute;
83
129
  private _currentParams;
84
130
  /**
@@ -109,7 +155,7 @@ export declare class Routes implements ReactiveController {
109
155
  * and the outlet ends up on a route the URL has already left. `Router`
110
156
  * threads `NavigateEvent.signal` through for exactly this reason.
111
157
  */
112
- goto(pathname: string, options?: {
158
+ goto(path: string, options?: {
113
159
  signal?: AbortSignal;
114
160
  }): Promise<void>;
115
161
  /**
@@ -133,7 +179,9 @@ export declare class Routes implements ReactiveController {
133
179
  * a genuine `enter()` rejection still surfaces the way it does upstream.
134
180
  * Skipping must still supersede: `goto()` is where the counter is bumped, so
135
181
  * returning without it would leave an in-flight child navigation current,
136
- * free to commit over a URL that has moved on.
182
+ * free to commit over a URL that has moved on. A parent route with no tail
183
+ * at all is the same case: nothing to route, but still something to stand
184
+ * down.
137
185
  *
138
186
  * No abort signal is threaded through, and the goto is deliberately not
139
187
  * awaited. The parent commits its own state before children run, so a child
@@ -149,7 +197,21 @@ export declare class Routes implements ReactiveController {
149
197
  */
150
198
  private _supersede;
151
199
  /**
152
- * True when this controller can render `pathname` — i.e. a route matches, or
200
+ * True when this controller, or any controller below it, has a route that
201
+ * constrains the hash.
202
+ *
203
+ * `Router` gates interception of fragment-only navigation on this. Left
204
+ * ungated, a pathname-only app would have every in-page anchor swallowed and
205
+ * re-rendered instead of scrolled; gated, such an app behaves exactly as it
206
+ * did before hash routes existed.
207
+ *
208
+ * Walks children because a nested controller may route on the hash while the
209
+ * top-level `Router` does not — and only the top-level one sees the
210
+ * navigate event.
211
+ */
212
+ protected _constrainsHash(seen?: Set<Routes>): boolean;
213
+ /**
214
+ * True when this controller can render `path` — i.e. a route matches, or
153
215
  * a fallback is configured.
154
216
  *
155
217
  * `Router` gates interception on this: intercepting a path we cannot render
@@ -157,7 +219,7 @@ export declare class Routes implements ReactiveController {
157
219
  * moved and the outlet stale. Letting the browser handle it instead means a
158
220
  * server-rendered page, an export endpoint, or a GET form still works.
159
221
  */
160
- hasRouteFor(pathname: string): boolean;
222
+ hasRouteFor(path: string): boolean;
161
223
  /**
162
224
  * Matches `pathname` against the installed routes and returns the first match
163
225
  * with its parsed parameters, or the fallback's match if one is configured.
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}