lit-navigation-router 0.4.0 → 0.6.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 +67 -2
- package/README.md +87 -25
- package/development/router.d.ts +17 -50
- package/development/router.d.ts.map +1 -1
- package/development/router.js +43 -93
- package/development/router.js.map +1 -1
- package/development/routes.d.ts +81 -87
- package/development/routes.d.ts.map +1 -1
- package/development/routes.js +338 -193
- package/development/routes.js.map +1 -1
- package/package.json +6 -6
- package/src/router.ts +49 -99
- package/src/routes.ts +417 -215
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`linkTo(name, params)` builds a URL from a route's `name`**
|
|
8
|
+
([#16](https://github.com/VanLandinghamLabs/lit-router/issues/16), inherited
|
|
9
|
+
from [lit/lit#3352](https://github.com/lit/lit/issues/3352)).
|
|
10
|
+
`RouteConfig.name` has existed since the fork began and was never read by
|
|
11
|
+
anything, so a component could only link by URL and had to know where its
|
|
12
|
+
parent was mounted. `linkTo` runs a route's pattern backwards and prefixes it
|
|
13
|
+
with that route's ancestors, so moving a subtree no longer breaks the links
|
|
14
|
+
inside it.
|
|
15
|
+
|
|
16
|
+
Resolution covers the mounted controller tree: the controller it is called
|
|
17
|
+
on, its ancestors, and any descendant that has rendered. A route in an
|
|
18
|
+
unmounted branch is not addressable, because the mapping from a parent route
|
|
19
|
+
to its child controller only exists once that parent has rendered. An unknown
|
|
20
|
+
name, a duplicated one, or a missing parameter throws rather than returning a
|
|
21
|
+
wrong URL.
|
|
22
|
+
|
|
23
|
+
Patterns are reversed for literals, `:name`, `*` and `{...}` groups, each
|
|
24
|
+
optionally `?`. A regex group cannot be reversed and `+` or `*` repetition
|
|
25
|
+
has no single answer, so both throw. A trailing wildcard may be omitted,
|
|
26
|
+
since the empty tail is the index of a nested route space; any other wildcard
|
|
27
|
+
is required.
|
|
28
|
+
|
|
29
|
+
## 0.5.0
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Routes can match on `search` and `hash`**
|
|
34
|
+
([#13](https://github.com/VanLandinghamLabs/lit-router/issues/13), inherited
|
|
35
|
+
from [lit/lit#3517](https://github.com/lit/lit/issues/3517)). Patterns were
|
|
36
|
+
executed as `exec({pathname})`, and `URLPattern` defaults every component the
|
|
37
|
+
caller omits to the empty string, so `new URLPattern({hash: 'one'})` was
|
|
38
|
+
compared against a hash of `''` and could never match. A hash- or
|
|
39
|
+
search-constrained route matched nothing at all, so `goto()` threw
|
|
40
|
+
`No route found` on every load instead of just failing to fire. Matching
|
|
41
|
+
now sees the whole location. Named groups captured from the search and hash
|
|
42
|
+
are merged into `params`; positional ones are not, since each component
|
|
43
|
+
numbers its groups from zero and a merged `"0"` would masquerade as a tail.
|
|
44
|
+
- **`goto()` is handed the search and hash.** `Router` called
|
|
45
|
+
`goto(window.location.pathname)` and `goto(url.pathname)`, so both were
|
|
46
|
+
discarded before matching ran. `goto()` now accepts and parses
|
|
47
|
+
`path?search#hash`, and passes them down to child controllers alongside the
|
|
48
|
+
tail.
|
|
49
|
+
- **Fragment-only navigation is routed when a route asks for it.** `Router`
|
|
50
|
+
declined every navigation with `hashChange` set, so a hash route could not be
|
|
51
|
+
reached by clicking a link even once matching was fixed. Such navigations are
|
|
52
|
+
now intercepted only when some route in the tree constrains the hash, so
|
|
53
|
+
pathname-only apps keep the browser's native in-page scrolling.
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
|
|
57
|
+
- `URLPatternLike` requires `hash`: the pattern string, which a real
|
|
58
|
+
`URLPattern` exposes and which is how `Router` decides whether a
|
|
59
|
+
fragment-only navigation is its business. Its `test()`/`exec()` take a
|
|
60
|
+
`{pathname, search, hash}` input, and `exec()` returns the `search` and
|
|
61
|
+
`hash` groups alongside the pathname's.
|
|
62
|
+
|
|
63
|
+
### Documentation
|
|
64
|
+
|
|
65
|
+
- The `URLPatternRouteConfig` doc comment claimed patterns were "limited to
|
|
66
|
+
checking `pathname` and `search`". `search` never worked either; both now do.
|
|
67
|
+
|
|
3
68
|
## 0.4.0
|
|
4
69
|
|
|
5
70
|
### Fixed
|
|
@@ -61,7 +126,7 @@
|
|
|
61
126
|
the second-to-last group as its tail.
|
|
62
127
|
|
|
63
128
|
**This changes behaviour.** If a route combines a wildcard with a param name
|
|
64
|
-
containing a digit, the child controller now receives a different path
|
|
129
|
+
containing a digit, the child controller now receives a different path, the
|
|
65
130
|
correct one. Anything relying on the old selection was relying on the child
|
|
66
131
|
being given the wrong segment.
|
|
67
132
|
|
|
@@ -80,7 +145,7 @@
|
|
|
80
145
|
running every pattern, and the fallback no longer rebuilds a `URLPattern` on
|
|
81
146
|
every navigation.
|
|
82
147
|
- `location.origin` is read per navigation rather than at module scope, so
|
|
83
|
-
importing the package no longer touches `location
|
|
148
|
+
importing the package no longer touches `location`. Importing it where there
|
|
84
149
|
is no DOM previously threw, contradicting `sideEffects: false`.
|
|
85
150
|
|
|
86
151
|
### Known issues
|
package/README.md
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# lit-navigation-router
|
|
2
2
|
|
|
3
|
+
[](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
|
-
Fork of [`@lit-labs/router`](https://github.com/lit/lit/tree/main/packages/labs/router)
|
|
6
|
-
|
|
8
|
+
Fork of [`@lit-labs/router`](https://github.com/lit/lit/tree/main/packages/labs/router).
|
|
9
|
+
See [NOTICE.md](./NOTICE.md) for provenance, licence and the full list of
|
|
7
10
|
changes. Not affiliated with or endorsed by Google or the Lit team.
|
|
8
11
|
|
|
9
12
|
```sh
|
|
@@ -59,9 +62,10 @@ await routes.goto('/item/1', {signal});
|
|
|
59
62
|
|
|
60
63
|
## Nested routes and tails
|
|
61
64
|
|
|
62
|
-
A route whose pattern ends in a wildcard
|
|
63
|
-
matched (the
|
|
64
|
-
tail has no leading slash, and child routes are written the
|
|
65
|
+
A route whose pattern ends in a wildcard, such as `/docs/*`, hands what the
|
|
66
|
+
wildcard matched (the tail) to any `Routes` controller mounted by its
|
|
67
|
+
`render()`. The tail has no leading slash, and child routes are written the
|
|
68
|
+
same way:
|
|
65
69
|
|
|
66
70
|
```ts
|
|
67
71
|
// Parent
|
|
@@ -75,7 +79,7 @@ private _routes = new Routes(this, [
|
|
|
75
79
|
```
|
|
76
80
|
|
|
77
81
|
The index of a nested route space is the empty tail, spelled `{path: ''}`.
|
|
78
|
-
`{path: '/'}` matches nothing there
|
|
82
|
+
`{path: '/'}` matches nothing there. It would need a tail of `/`, so a URL of
|
|
79
83
|
`/docs//`.
|
|
80
84
|
|
|
81
85
|
Only a trailing wildcard produces a tail. An unnamed regex group
|
|
@@ -85,25 +89,83 @@ children nor stripped from `link()`. A `fallback` behaves like a `/*` route and
|
|
|
85
89
|
passes the whole pathname on as the tail. Nested, it accepts the slash-less
|
|
86
90
|
tail it is handed and passes that on.
|
|
87
91
|
|
|
88
|
-
##
|
|
92
|
+
## Search and hash routes
|
|
93
|
+
|
|
94
|
+
A `pattern` route can constrain `search` and `hash` as well as `pathname`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
{pattern: new URLPattern({pathname: '/docs', hash: ':section'}),
|
|
98
|
+
render: ({section}) => html`<doc-page .section=${section}></doc-page>`}
|
|
99
|
+
|
|
100
|
+
{pattern: new URLPattern({pathname: '/search', search: 'q=:term'}),
|
|
101
|
+
render: ({term}) => html`<results-for .term=${term}></results-for>`}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Named groups captured from the search and hash join the pathname's in `params`.
|
|
105
|
+
Positional ones do not: every component numbers its groups from zero
|
|
106
|
+
independently, so merging them would make a fragment indistinguishable from a
|
|
107
|
+
tail. Name the groups you need.
|
|
108
|
+
|
|
109
|
+
Only the pathname nests. A child controller is handed the parent's tail as its
|
|
110
|
+
pathname and the same search and hash. There is no way to split a query string
|
|
111
|
+
or a fragment across a route tree.
|
|
112
|
+
|
|
113
|
+
Fragment-only navigation (`<a href="#section">`) is intercepted only when some
|
|
114
|
+
route constrains the hash. An app that routes on pathnames alone keeps the
|
|
115
|
+
browser's native in-page scrolling.
|
|
116
|
+
|
|
117
|
+
## Named routes
|
|
118
|
+
|
|
119
|
+
Give a route a `name` and link to it by that name instead of by URL, so a
|
|
120
|
+
component does not have to know where the route currently lives:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
{name: 'page', path: ':page', render: ({page}) => html`<doc-page .page=${page}></doc-page>`}
|
|
124
|
+
|
|
125
|
+
// Anywhere in the mounted tree:
|
|
126
|
+
html`<a href="${this._routes.linkTo('page', {page: 'intro'})}">Intro</a>`
|
|
127
|
+
// => /docs/intro
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`linkTo(name, params)` runs the route's pattern backwards and prefixes it with
|
|
131
|
+
the route's ancestors, so moving a subtree from `/docs/*` to `/guide/*` does not
|
|
132
|
+
break the links inside it. To navigate rather than render an href, hand the
|
|
133
|
+
result to `goto()`.
|
|
134
|
+
|
|
135
|
+
Resolution covers the mounted controller tree: the controller you call it on,
|
|
136
|
+
its ancestors, and any descendant that has rendered. A route in a branch that
|
|
137
|
+
has not mounted yet is not addressable, because the mapping from a parent route
|
|
138
|
+
to its child controller only exists once that parent has rendered. An unknown
|
|
139
|
+
name, an ambiguous one, or a missing parameter throws rather than producing a
|
|
140
|
+
wrong URL.
|
|
141
|
+
|
|
142
|
+
A trailing wildcard may be left out, since the empty tail is the index of a
|
|
143
|
+
nested route space: `linkTo('docs')` on `{name: 'docs', path: '/docs/*'}` gives
|
|
144
|
+
`/docs/`. Any other wildcard is required, because dropping it would silently
|
|
145
|
+
join the segments around it.
|
|
146
|
+
|
|
147
|
+
Patterns are reversed for literals, `:name`, `*` and `{...}` groups, each
|
|
148
|
+
optionally `?`. A regex group cannot be reversed and `+` or `*` repetition has
|
|
149
|
+
no single answer, so both throw. Name the groups you want to link to.
|
|
150
|
+
|
|
151
|
+
## Browser support, please read
|
|
89
152
|
|
|
90
|
-
This router
|
|
153
|
+
This router requires the Navigation API. There is no legacy fallback.
|
|
91
154
|
|
|
92
|
-
The API is Baseline
|
|
93
|
-
Firefox 147). Baseline
|
|
94
|
-
|
|
155
|
+
The API is Baseline Newly Available (January 2026: Chrome/Edge, Safari 26.2,
|
|
156
|
+
Firefox 147). Baseline Widely Available is not until roughly mid-2028, so older
|
|
157
|
+
engines are still in the wild.
|
|
95
158
|
|
|
96
159
|
On an engine without it, `Router` renders the current route on load but does not
|
|
97
|
-
intercept navigation
|
|
160
|
+
intercept navigation, so every link becomes an ordinary full page load. If your
|
|
98
161
|
server serves the app shell on every route that is slower, not broken. If it
|
|
99
162
|
does not, those links 404.
|
|
100
163
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
degradation.
|
|
164
|
+
One case is broken rather than slow. If you navigate programmatically with
|
|
165
|
+
`history.pushState()` plus `router.goto()`, nothing listens for the resulting
|
|
166
|
+
`popstate`, so Back moves the URL while the outlet stays put. That is the
|
|
167
|
+
URL/outlet split this fork exists to eliminate. Apps using that pattern should
|
|
168
|
+
gate on `supportsNavigationApi()` rather than accept the degradation.
|
|
107
169
|
|
|
108
170
|
`supportsNavigationApi()` is exported so you can detect this at boot:
|
|
109
171
|
|
|
@@ -116,15 +178,15 @@ if (!supportsNavigationApi()) {
|
|
|
116
178
|
```
|
|
117
179
|
|
|
118
180
|
If you need real pre-2026 support, pair this with a Navigation API polyfill
|
|
119
|
-
rather than a second router
|
|
120
|
-
a layer whose whole job is spec accuracy.
|
|
181
|
+
rather than a second router. That keeps one decision path, with compatibility
|
|
182
|
+
isolated in a layer whose whole job is spec accuracy.
|
|
121
183
|
|
|
122
184
|
## `URLPattern`
|
|
123
185
|
|
|
124
186
|
Route patterns are compiled with `URLPattern`, which this package uses from the
|
|
125
|
-
global scope and does
|
|
126
|
-
|
|
127
|
-
|
|
187
|
+
global scope and does not polyfill, same as upstream. Every engine that has the
|
|
188
|
+
Navigation API also has `URLPattern`, so if the support check above passes you
|
|
189
|
+
need nothing. If you support older engines anyway, load
|
|
128
190
|
[`urlpattern-polyfill`](https://www.npmjs.com/package/urlpattern-polyfill)
|
|
129
191
|
before the router:
|
|
130
192
|
|
|
@@ -135,14 +197,14 @@ if (!globalThis.URLPattern) {
|
|
|
135
197
|
}
|
|
136
198
|
```
|
|
137
199
|
|
|
138
|
-
The published types do not depend on it
|
|
200
|
+
The published types do not depend on it. `URLPatternRouteConfig.pattern` is
|
|
139
201
|
typed structurally, so a consumer build never needs the polyfill's types.
|
|
140
202
|
|
|
141
203
|
## Develop
|
|
142
204
|
|
|
143
205
|
```sh
|
|
144
206
|
npm install
|
|
145
|
-
npm run build # tsc
|
|
207
|
+
npm run build # tsc to development/
|
|
146
208
|
npm test # Web Test Runner (Chromium via Playwright)
|
|
147
209
|
npm run check-types
|
|
148
210
|
```
|
package/development/router.d.ts
CHANGED
|
@@ -13,61 +13,28 @@ export interface InterceptOptions {
|
|
|
13
13
|
scroll?: 'after-transition' | 'manual';
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* True when the Navigation API is available
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* This router **requires** it. Exported so an app can detect an unsupported
|
|
20
|
-
* engine at boot and say so, rather than leaving the user to notice that every
|
|
21
|
-
* link reloads the page.
|
|
16
|
+
* True when the Navigation API is available (Baseline Newly Available, January
|
|
17
|
+
* 2026). This router requires it; exported so an app can detect an unsupported
|
|
18
|
+
* engine at boot rather than leaving users to notice every link reloading.
|
|
22
19
|
*/
|
|
23
20
|
export declare const supportsNavigationApi: () => boolean;
|
|
24
21
|
/**
|
|
25
|
-
* A root-level router that intercepts navigation via the Navigation API.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* moved, but `goto()` awaits `route.enter()` before swapping the outlet. The
|
|
39
|
-
* URL leads and the outlet lags, leaving two sources of truth — the outgoing
|
|
40
|
-
* route re-renders with stale params, and two quick navigations commit in
|
|
41
|
-
* whatever order their `enter()` hooks happen to resolve.
|
|
42
|
-
*
|
|
43
|
-
* `navigateEvent.intercept({handler})` collapses that. The browser commits the
|
|
44
|
-
* URL and holds the navigation un-finished while the handler runs, and it
|
|
45
|
-
* aborts `navigateEvent.signal` when a newer navigation supersedes this one —
|
|
46
|
-
* which `goto()` honours, so a superseded route can no longer win the outlet.
|
|
47
|
-
*
|
|
48
|
-
* ## No legacy fallback
|
|
49
|
-
*
|
|
50
|
-
* An earlier version of this fork kept upstream's click/popstate path for
|
|
51
|
-
* pre-2026 engines. It was removed deliberately. Ten review rounds found
|
|
52
|
-
* divergences between the two paths and **every one was in the click handler**,
|
|
53
|
-
* never in this one — which is structural, not luck: this handler reads a
|
|
54
|
-
* decision the browser has already made, while the click handler had to
|
|
55
|
-
* re-derive it, re-implementing the rules for choosing a navigable, the
|
|
56
|
-
* fragment-navigation predicate, and the modifier-key rules. Each round found
|
|
57
|
-
* another place where the re-implementation and the spec disagreed.
|
|
58
|
-
*
|
|
59
|
-
* On an engine without the API, links fall back to ordinary full page loads.
|
|
60
|
-
* For an app whose server serves the shell on every route that still works —
|
|
61
|
-
* it is slower, not broken — and `supportsNavigationApi()` lets you detect it.
|
|
62
|
-
* If real pre-2026 support is ever needed, use a Navigation API polyfill: one
|
|
63
|
-
* decision path, with compatibility isolated in a layer whose whole job is
|
|
64
|
-
* spec accuracy.
|
|
22
|
+
* A root-level router that intercepts navigation via the Navigation API. Use one
|
|
23
|
+
* per page, since it installs a global listener, and `Routes` for nested spaces.
|
|
24
|
+
*
|
|
25
|
+
* Upstream committed with `history.pushState()` and reacted to `popstate`, but
|
|
26
|
+
* swapped the outlet only after awaiting `enter()`. The URL led and the outlet
|
|
27
|
+
* lagged, so the outgoing route re-rendered with stale params and quick
|
|
28
|
+
* navigations committed out of order. `intercept({handler})` collapses that:
|
|
29
|
+
* the browser holds the navigation unfinished while the handler runs and aborts
|
|
30
|
+
* `signal` when a newer one supersedes it.
|
|
31
|
+
*
|
|
32
|
+
* There is no legacy fallback for pre-2026 engines. Without the API links
|
|
33
|
+
* become full page loads, which `supportsNavigationApi()` detects. See the
|
|
34
|
+
* README for what that degrades and what it breaks.
|
|
65
35
|
*/
|
|
66
36
|
export declare class Router extends Routes {
|
|
67
|
-
/**
|
|
68
|
-
* Options forwarded to `navigateEvent.intercept()`. Leaving these unset
|
|
69
|
-
* gives the browser's default scroll and focus handling.
|
|
70
|
-
*/
|
|
37
|
+
/** Forwarded to `navigateEvent.intercept()`. Unset uses browser defaults. */
|
|
71
38
|
interceptOptions?: InterceptOptions;
|
|
72
39
|
private _listening;
|
|
73
40
|
hostConnected(): void;
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAoBnC,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAC;IAC3C,MAAM,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAC;CACxC;AAoBD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAAO,OAEgB,CAAC;AAE1D;;;;;;;;;;;;;;GAcG;AACH,qBAAa,MAAO,SAAQ,MAAM;IAChC,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAEpC,OAAO,CAAC,UAAU,CAAS;IAElB,aAAa,SAerB;IAEQ,gBAAgB,SAMxB;IAED;;;OAGG;IACH,OAAO,CAAC,WAAW,CAgDjB;CACH"}
|
package/development/router.js
CHANGED
|
@@ -8,83 +8,45 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Routes } from './routes.js';
|
|
10
10
|
const getNavigation = () => window.navigation;
|
|
11
|
+
/** The current location in the form `goto()` parses, minus the origin. */
|
|
12
|
+
const currentPath = () => window.location.pathname + window.location.search + window.location.hash;
|
|
11
13
|
/**
|
|
12
|
-
* True when the Navigation API is available
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* This router **requires** it. Exported so an app can detect an unsupported
|
|
16
|
-
* engine at boot and say so, rather than leaving the user to notice that every
|
|
17
|
-
* link reloads the page.
|
|
14
|
+
* True when the Navigation API is available (Baseline Newly Available, January
|
|
15
|
+
* 2026). This router requires it; exported so an app can detect an unsupported
|
|
16
|
+
* engine at boot rather than leaving users to notice every link reloading.
|
|
18
17
|
*/
|
|
19
18
|
export const supportsNavigationApi = () => typeof window !== 'undefined' &&
|
|
20
19
|
typeof getNavigation()?.addEventListener === 'function';
|
|
21
20
|
/**
|
|
22
|
-
* A root-level router that intercepts navigation via the Navigation API.
|
|
23
|
-
*
|
|
24
|
-
* This class extends Routes so that it can also have a route configuration.
|
|
25
|
-
*
|
|
26
|
-
* There should only be one Router instance on a page, since the Router
|
|
27
|
-
* installs a global listener. Nested routes should be configured with the
|
|
28
|
-
* `Routes` class.
|
|
29
|
-
*
|
|
30
|
-
* ## Why the Navigation API
|
|
31
|
-
*
|
|
32
|
-
* Upstream intercepted navigation with a global click listener plus `popstate`
|
|
33
|
-
* and committed with `history.pushState()`. That is structurally racy:
|
|
34
|
-
* `pushState` is synchronous and `popstate` fires *after* the URL has already
|
|
35
|
-
* moved, but `goto()` awaits `route.enter()` before swapping the outlet. The
|
|
36
|
-
* URL leads and the outlet lags, leaving two sources of truth — the outgoing
|
|
37
|
-
* route re-renders with stale params, and two quick navigations commit in
|
|
38
|
-
* whatever order their `enter()` hooks happen to resolve.
|
|
39
|
-
*
|
|
40
|
-
* `navigateEvent.intercept({handler})` collapses that. The browser commits the
|
|
41
|
-
* URL and holds the navigation un-finished while the handler runs, and it
|
|
42
|
-
* aborts `navigateEvent.signal` when a newer navigation supersedes this one —
|
|
43
|
-
* which `goto()` honours, so a superseded route can no longer win the outlet.
|
|
21
|
+
* A root-level router that intercepts navigation via the Navigation API. Use one
|
|
22
|
+
* per page, since it installs a global listener, and `Routes` for nested spaces.
|
|
44
23
|
*
|
|
45
|
-
*
|
|
24
|
+
* Upstream committed with `history.pushState()` and reacted to `popstate`, but
|
|
25
|
+
* swapped the outlet only after awaiting `enter()`. The URL led and the outlet
|
|
26
|
+
* lagged, so the outgoing route re-rendered with stale params and quick
|
|
27
|
+
* navigations committed out of order. `intercept({handler})` collapses that:
|
|
28
|
+
* the browser holds the navigation unfinished while the handler runs and aborts
|
|
29
|
+
* `signal` when a newer one supersedes it.
|
|
46
30
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* never in this one — which is structural, not luck: this handler reads a
|
|
51
|
-
* decision the browser has already made, while the click handler had to
|
|
52
|
-
* re-derive it, re-implementing the rules for choosing a navigable, the
|
|
53
|
-
* fragment-navigation predicate, and the modifier-key rules. Each round found
|
|
54
|
-
* another place where the re-implementation and the spec disagreed.
|
|
55
|
-
*
|
|
56
|
-
* On an engine without the API, links fall back to ordinary full page loads.
|
|
57
|
-
* For an app whose server serves the shell on every route that still works —
|
|
58
|
-
* it is slower, not broken — and `supportsNavigationApi()` lets you detect it.
|
|
59
|
-
* If real pre-2026 support is ever needed, use a Navigation API polyfill: one
|
|
60
|
-
* decision path, with compatibility isolated in a layer whose whole job is
|
|
61
|
-
* spec accuracy.
|
|
31
|
+
* There is no legacy fallback for pre-2026 engines. Without the API links
|
|
32
|
+
* become full page loads, which `supportsNavigationApi()` detects. See the
|
|
33
|
+
* README for what that degrades and what it breaks.
|
|
62
34
|
*/
|
|
63
35
|
export class Router extends Routes {
|
|
64
|
-
/**
|
|
65
|
-
* Options forwarded to `navigateEvent.intercept()`. Leaving these unset
|
|
66
|
-
* gives the browser's default scroll and focus handling.
|
|
67
|
-
*/
|
|
36
|
+
/** Forwarded to `navigateEvent.intercept()`. Unset uses browser defaults. */
|
|
68
37
|
interceptOptions;
|
|
69
38
|
_listening = false;
|
|
70
39
|
hostConnected() {
|
|
71
40
|
super.hostConnected();
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// branch throw out of connectedCallback while `supportsNavigationApi()`
|
|
75
|
-
// told the app it was unsupported — and then even the initial render below
|
|
76
|
-
// would not run.
|
|
41
|
+
// The predicate, not `navigation !== undefined`: a partial polyfill would
|
|
42
|
+
// otherwise throw out of connectedCallback and skip the render below.
|
|
77
43
|
if (supportsNavigationApi()) {
|
|
78
44
|
getNavigation().addEventListener('navigate', this._onNavigate);
|
|
79
45
|
this._listening = true;
|
|
80
46
|
}
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
// Surfaced rather than left as a bare unhandled rejection, matching the
|
|
85
|
-
// convention in routes.ts: on an engine without the API this is the *only*
|
|
86
|
-
// rendering path, and a deep link with no matching route throws here.
|
|
87
|
-
void this.goto(window.location.pathname).catch((err) => {
|
|
47
|
+
// Runs even without the API, which is what makes the degradation slow
|
|
48
|
+
// rather than blank. Surfaced because it is then the only rendering path.
|
|
49
|
+
void this.goto(currentPath()).catch((err) => {
|
|
88
50
|
queueMicrotask(() => {
|
|
89
51
|
throw err;
|
|
90
52
|
});
|
|
@@ -102,57 +64,45 @@ export class Router extends Routes {
|
|
|
102
64
|
* `navigation.navigate()`, `history.pushState()`, and back/forward.
|
|
103
65
|
*/
|
|
104
66
|
_onNavigate = (e) => {
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
e.formData != null) {
|
|
67
|
+
// `!= null`: a polyfill leaving either unset would make a strict check
|
|
68
|
+
// true for every link and silently decline the whole app.
|
|
69
|
+
if (!e.canIntercept || e.downloadRequest != null || e.formData != null) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Fragment-only moves are the browser's unless a route reads the fragment.
|
|
73
|
+
// Always intercepting costs native scrolling; never intercepting is
|
|
74
|
+
// lit/lit#3517. See #13.
|
|
75
|
+
if (e.hashChange && !this._constrainsHash()) {
|
|
115
76
|
return;
|
|
116
77
|
}
|
|
117
78
|
// Reloads must stay reloads. `canIntercept` is true for them, so without
|
|
118
|
-
// this `location.reload()`
|
|
119
|
-
// same path — the document is never replaced, breaking the standard
|
|
120
|
-
// "new version available, reload" escape hatch. (It would also disagree
|
|
121
|
-
// with the browser's own refresh button, which is not interceptable.)
|
|
79
|
+
// this `location.reload()` never replaces the document.
|
|
122
80
|
if (e.navigationType === 'reload') {
|
|
123
81
|
return;
|
|
124
82
|
}
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// for us. Best-effort: `sourceElement` is not in every engine, and is
|
|
128
|
-
// absent for programmatic navigation.
|
|
83
|
+
// This router's convention, not HTML's, so the browser will not decline
|
|
84
|
+
// these for us. Best-effort: `sourceElement` is not everywhere.
|
|
129
85
|
if (e.sourceElement?.getAttribute?.('rel') === 'external') {
|
|
130
86
|
return;
|
|
131
87
|
}
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// where there is no `location` (SSR, a bundler evaluating for tree-shaking
|
|
135
|
-
// under the package's `sideEffects: false` claim).
|
|
88
|
+
// Per navigation, not module scope: reading `location` on import throws
|
|
89
|
+
// under SSR, contradicting the package's `sideEffects: false`.
|
|
136
90
|
const url = new URL(e.destination.url);
|
|
137
91
|
if (url.origin !== window.location.origin) {
|
|
138
92
|
return;
|
|
139
93
|
}
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// the outlet never went. Declining lets the browser do the real
|
|
146
|
-
// navigation, which is the correct outcome.
|
|
147
|
-
if (!this.hasRouteFor(url.pathname)) {
|
|
94
|
+
// `canIntercept` is true for cross-document same-origin URLs too, so
|
|
95
|
+
// without this a server-rendered page or GET form gets swallowed: the URL
|
|
96
|
+
// commits, goto() throws, and the outlet never moves.
|
|
97
|
+
const path = url.pathname + url.search + url.hash;
|
|
98
|
+
if (!this.hasRouteFor(path)) {
|
|
148
99
|
return;
|
|
149
100
|
}
|
|
150
101
|
e.intercept({
|
|
151
102
|
...this.interceptOptions,
|
|
152
103
|
handler: async () => {
|
|
153
|
-
// `e.signal` aborts if
|
|
154
|
-
|
|
155
|
-
await this.goto(url.pathname, { signal: e.signal });
|
|
104
|
+
// `e.signal` aborts if a newer navigation starts; goto() checks it.
|
|
105
|
+
await this.goto(path, { signal: e.signal });
|
|
156
106
|
},
|
|
157
107
|
});
|
|
158
108
|
};
|
|
@@ -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":"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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. Typed rather than `any`\n * because these properties are the correctness boundary: `hashchange` for\n * `hashChange` would silently disable a filter forever.\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/** The current location in the form `goto()` parses, minus the origin. */\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, January\n * 2026). This router requires it; exported so an app can detect an unsupported\n * engine at boot rather than leaving users to notice every link reloading.\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. Use one\n * per page, since it installs a global listener, and `Routes` for nested spaces.\n *\n * Upstream committed with `history.pushState()` and reacted to `popstate`, but\n * swapped the outlet only after awaiting `enter()`. The URL led and the outlet\n * lagged, so the outgoing route re-rendered with stale params and quick\n * navigations committed out of order. `intercept({handler})` collapses that:\n * the browser holds the navigation unfinished while the handler runs and aborts\n * `signal` when a newer one supersedes it.\n *\n * There is no legacy fallback for pre-2026 engines. Without the API links\n * become full page loads, which `supportsNavigationApi()` detects. See the\n * README for what that degrades and what it breaks.\n */\nexport class Router extends Routes {\n /** Forwarded to `navigateEvent.intercept()`. Unset uses browser defaults. */\n interceptOptions?: InterceptOptions;\n\n private _listening = false;\n\n override hostConnected() {\n super.hostConnected();\n // The predicate, not `navigation !== undefined`: a partial polyfill would\n // otherwise throw out of connectedCallback and skip the render below.\n if (supportsNavigationApi()) {\n getNavigation()!.addEventListener('navigate', this._onNavigate);\n this._listening = true;\n }\n // Runs even without the API, which is what makes the degradation slow\n // rather than blank. Surfaced because it is then the only rendering path.\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 // `!= null`: a polyfill leaving either unset would make a strict check\n // true for every 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 are the browser's unless a route reads the fragment.\n // Always intercepting costs native scrolling; never intercepting is\n // lit/lit#3517. See #13.\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()` never replaces the document.\n if (e.navigationType === 'reload') {\n return;\n }\n\n // This router's convention, not HTML's, so the browser will not decline\n // these for us. Best-effort: `sourceElement` is not everywhere.\n if (e.sourceElement?.getAttribute?.('rel') === 'external') {\n return;\n }\n\n // Per navigation, not module scope: reading `location` on import throws\n // under SSR, contradicting the package's `sideEffects: false`.\n const url = new URL(e.destination.url);\n if (url.origin !== window.location.origin) {\n return;\n }\n\n // `canIntercept` is true for cross-document same-origin URLs too, so\n // without this a server-rendered page or GET form gets swallowed: the URL\n // commits, goto() throws, and the outlet never moves.\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 a newer navigation starts; goto() checks it.\n await this.goto(path, {signal: e.signal});\n },\n });\n };\n}\n"]}
|