lit-navigation-router 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/NOTICE.md +168 -0
- package/README.md +120 -0
- package/development/index.d.ts +9 -0
- package/development/index.d.ts.map +1 -0
- package/development/index.js +8 -0
- package/development/index.js.map +1 -0
- package/development/router.d.ts +81 -0
- package/development/router.d.ts.map +1 -0
- package/development/router.js +154 -0
- package/development/router.js.map +1 -0
- package/development/routes.d.ts +163 -0
- package/development/routes.d.ts.map +1 -0
- package/development/routes.js +363 -0
- package/development/routes.js.map +1 -0
- package/package.json +72 -0
- package/src/index.ts +8 -0
- package/src/router.ts +205 -0
- package/src/routes.ts +471 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Google LLC. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Provenance
|
|
2
|
+
|
|
3
|
+
This is a fork of **`@lit-labs/router`**, which is not a standalone repository —
|
|
4
|
+
it lives inside the `lit/lit` monorepo at `packages/labs/router`.
|
|
5
|
+
|
|
6
|
+
| | |
|
|
7
|
+
|---|---|
|
|
8
|
+
| Upstream | https://github.com/lit/lit/tree/main/packages/labs/router |
|
|
9
|
+
| Upstream version | `@lit-labs/router@0.1.4` (latest published at fork time) |
|
|
10
|
+
| Upstream commit | `c42ee1e96b8fd61f7256f61d715daef572e76e52` |
|
|
11
|
+
| Forked | 2026-07-31 |
|
|
12
|
+
| Licence | BSD-3-Clause — unchanged, see `LICENSE` |
|
|
13
|
+
|
|
14
|
+
Original copyright **Google LLC**, retained verbatim in `LICENSE` and in every
|
|
15
|
+
per-file header. BSD-3-Clause requires retention of the copyright notice, the
|
|
16
|
+
conditions and the disclaimer — it does *not* require a modification notice
|
|
17
|
+
(that is Apache-2.0 §4(b)). Modified files carry one anyway, as a courtesy to
|
|
18
|
+
anyone diffing against upstream.
|
|
19
|
+
|
|
20
|
+
Published as `lit-navigation-router`. Deliberately *not* named
|
|
21
|
+
`@lit-labs/router` — that name belongs to the Lit team, and a fork
|
|
22
|
+
masquerading as upstream would be worse than useless when debugging. This
|
|
23
|
+
package is not affiliated with or endorsed by Google or the Lit team; the
|
|
24
|
+
reference to Lit describes what it is for.
|
|
25
|
+
|
|
26
|
+
In the source repository (not the published tarball), `UPSTREAM-README.md` and
|
|
27
|
+
`UPSTREAM-CHANGELOG.md` are upstream's, kept verbatim for reference, and
|
|
28
|
+
`.upstream-commit` records the fork point.
|
|
29
|
+
|
|
30
|
+
## Why fork
|
|
31
|
+
|
|
32
|
+
Upstream `Router` intercepts navigation with a global click listener plus
|
|
33
|
+
`popstate`, and commits with `history.pushState()`. That has a structural
|
|
34
|
+
flaw: `pushState` is synchronous and `popstate` fires *after* the URL has
|
|
35
|
+
already moved, but `Routes.goto()` awaits `route.enter()` before swapping the
|
|
36
|
+
outlet. The URL leads and the outlet lags, which leaves two sources of truth.
|
|
37
|
+
|
|
38
|
+
In arcsync that produced a run of bugs with one root cause — the outgoing route
|
|
39
|
+
re-rendering with stale params, and quick successive navigations committing in
|
|
40
|
+
whichever order their `enter()` hooks happened to resolve:
|
|
41
|
+
|
|
42
|
+
- `VanLandinghamLabs/arcsync#258` — direct load of `/markdown/:id` degraded to a demo route
|
|
43
|
+
- `VanLandinghamLabs/arcsync#632` — route commits landed in chunk-download order
|
|
44
|
+
- `VanLandinghamLabs/arcsync#640` — a route re-entered its own loader
|
|
45
|
+
- Tracking issue: `VanLandinghamLabs/arcsync#648`
|
|
46
|
+
|
|
47
|
+
Upstream's own source carries the matching TODO, removed in this fork because
|
|
48
|
+
it is now answered:
|
|
49
|
+
|
|
50
|
+
> `// TODO (justinfagnani): do we need to detect when goto() is called while a previous goto() call is still pending?`
|
|
51
|
+
|
|
52
|
+
## Changes from upstream
|
|
53
|
+
|
|
54
|
+
### `src/router.ts` — rewritten on the Navigation API
|
|
55
|
+
|
|
56
|
+
- Listens for `navigate` on `window.navigation` and calls
|
|
57
|
+
`navigateEvent.intercept({handler})`. The browser commits the URL and holds
|
|
58
|
+
the navigation un-finished while `goto()` runs, so URL and outlet move
|
|
59
|
+
together.
|
|
60
|
+
- Threads `navigateEvent.signal` into `goto()` so a superseded navigation
|
|
61
|
+
stands down instead of racing.
|
|
62
|
+
- Covers navigation the legacy path could not see at all: `navigation.navigate()`,
|
|
63
|
+
`history.pushState()`, and traversals — not just anchor clicks and popstate.
|
|
64
|
+
- Skips what isn't ours: `!canIntercept`, `hashChange`, `downloadRequest`,
|
|
65
|
+
form submissions, and cross-origin destinations.
|
|
66
|
+
- New `interceptOptions` field forwards `focusReset` / `scroll` to `intercept()`.
|
|
67
|
+
- **The legacy click/popstate path is deleted.** Ten review rounds found
|
|
68
|
+
divergences between it and `_onNavigate`, and every one was in the click
|
|
69
|
+
handler — structural, not luck: `_onNavigate` reads a decision the browser
|
|
70
|
+
already made, while the click handler re-derived it, re-implementing the
|
|
71
|
+
rules for choosing a navigable, the fragment-navigation predicate and the
|
|
72
|
+
modifier-key rules. Each round found another place the re-implementation and
|
|
73
|
+
the spec disagreed. On an engine without the API links become ordinary full
|
|
74
|
+
page loads (slower, not broken, for a server that serves the shell on every
|
|
75
|
+
route); `supportsNavigationApi()` is exported so an app can detect it.
|
|
76
|
+
|
|
77
|
+
### `src/routes.ts` — behavioural changes
|
|
78
|
+
|
|
79
|
+
- `hostDisconnected` removes the `lit-routes-connected` listener that
|
|
80
|
+
`hostConnected` adds. Upstream leaves it, so a host that is disconnected and
|
|
81
|
+
reconnected (a `repeat()` reorder, a tab swap) ends up with two sibling
|
|
82
|
+
controllers registered as each other's child — a real `_childRoutes` cycle,
|
|
83
|
+
which turns `goto()`'s propagation loop and any recursive walk into a stack
|
|
84
|
+
overflow. `_supersede()` also carries a visited set as defence in depth.
|
|
85
|
+
|
|
86
|
+
- `goto(pathname, options?)` accepts `options.signal`; an aborted signal after
|
|
87
|
+
`enter()` resolves makes `goto()` return without committing.
|
|
88
|
+
- **Per-controller last-goto-wins counter.** The signal alone is not enough: a
|
|
89
|
+
child controller mounts as a *result* of its parent's render, so its first
|
|
90
|
+
`goto()` comes from `_onRoutesConnected` — after the parent's navigation has
|
|
91
|
+
already finished, holding a signal that will never abort. Without the counter
|
|
92
|
+
a slow first child load commits over a newer one. This also keeps `Routes`
|
|
93
|
+
correct standalone, with no `Router` and no Navigation API involved.
|
|
94
|
+
- Child `goto()`s stay **unawaited** (as upstream) and, like upstream, are given
|
|
95
|
+
**no abort signal** — the parent commits before children run, so a child
|
|
96
|
+
handed an aborted signal stands down with no newer `goto()` to correct it and
|
|
97
|
+
the nested outlet sticks. A child with no route for the new tail (the outgoing
|
|
98
|
+
branch, mid-swap) is skipped structurally via `hasRouteFor`, so a genuine
|
|
99
|
+
`enter()` rejection still surfaces rather than being swallowed. An
|
|
100
|
+
earlier revision awaited them to make `navigation.finished` cover the whole
|
|
101
|
+
tree; that was wrong twice over — at that point `requestUpdate()` has not run,
|
|
102
|
+
so `_childRoutes` still holds the *outgoing* branch, and awaiting it both
|
|
103
|
+
gated the parent's outlet swap on an `enter()` for a tail that child would
|
|
104
|
+
never render, and let its `No route found` throw strand the parent entirely.
|
|
105
|
+
Nested supersession is the counter's job, not the await's.
|
|
106
|
+
- New `hasRouteFor(pathname)`, used by `Router` to decline what it cannot
|
|
107
|
+
render.
|
|
108
|
+
|
|
109
|
+
### Known limits
|
|
110
|
+
|
|
111
|
+
- `navigation.finished` covers the top-level route, not nested ones. Making it
|
|
112
|
+
cover the tree needs awaiting the children the navigation is moving *to*,
|
|
113
|
+
which means awaiting past `requestUpdate()` and the host's update cycle — a
|
|
114
|
+
materially bigger change than it looks.
|
|
115
|
+
- `hasRouteFor()` is a no-op for apps that configure a root `fallback`, since
|
|
116
|
+
every path then matches. That is the app's stated intent, but it means the
|
|
117
|
+
"decline what we cannot render" fix does not reach that configuration.
|
|
118
|
+
- `goto()` still ignores the query string (upstream limitation), so a GET form
|
|
119
|
+
whose action matches a route is intercepted and loses its query.
|
|
120
|
+
- The `seen` set in `_supersede()` is defence in depth and unpinned **by
|
|
121
|
+
construction**: since `hostDisconnected` removes its listener, no test can
|
|
122
|
+
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
|
|
126
|
+
had the same end state by a different route (`No route found` threw before
|
|
127
|
+
any state changed). Clearing it needs `_currentRoute` reset plus a host
|
|
128
|
+
update, which is a behaviour change rather than a bug fix.
|
|
129
|
+
|
|
130
|
+
### Build and tests
|
|
131
|
+
|
|
132
|
+
Upstream's package is wired into the lit monorepo (wireit, `@lit-internal/scripts`,
|
|
133
|
+
`treemirror`, `../../tests`). This fork replaces that with plain `tsc` + Web Test
|
|
134
|
+
Runner so it stands alone. Test changes:
|
|
135
|
+
|
|
136
|
+
- Imports rewritten from `@lit-labs/router/*` to relative paths.
|
|
137
|
+
- `stripExpressionComments` reimplemented locally (`src/test/test-helpers.ts`)
|
|
138
|
+
in place of the monorepo-internal `@lit-labs/testing`.
|
|
139
|
+
- `chai` → `@open-wc/testing` (browser-native ESM).
|
|
140
|
+
- Two `(r: RouteConfig)` annotations added in `router_test.ts` (upstream relied
|
|
141
|
+
on monorepo-wide inference). **Otherwise upstream's 6 tests are unmodified and
|
|
142
|
+
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.
|
|
144
|
+
The first asserts the suite is actually running against `window.navigation`
|
|
145
|
+
rather than silently falling back — without it the rest would pass against the
|
|
146
|
+
legacy path and prove nothing. It is kept as a guard for the day this
|
|
147
|
+
package's requirement changes.
|
|
148
|
+
|
|
149
|
+
## Verified
|
|
150
|
+
|
|
151
|
+
`npm test` → 20 passed (6 upstream + 14 new), Chromium via Playwright.
|
|
152
|
+
|
|
153
|
+
Run `npm run clean` before a mutation check: the build is `composite`/
|
|
154
|
+
`incremental`, and a stale `development/` can contain a hunk's comment without
|
|
155
|
+
its code, greening the suite against output that lacks the change.
|
|
156
|
+
|
|
157
|
+
## Merging upstream later
|
|
158
|
+
|
|
159
|
+
`.upstream-commit` in the repository records the fork point. To pull upstream
|
|
160
|
+
changes:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
git clone --depth 1 --filter=blob:none --sparse https://github.com/lit/lit.git
|
|
164
|
+
cd lit && git sparse-checkout set packages/labs/router
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
then diff `packages/labs/router/src` against this repo's `src`. Only
|
|
168
|
+
`router.ts` and `goto()` in `routes.ts` diverge meaningfully.
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# lit-navigation-router
|
|
2
|
+
|
|
3
|
+
A router for Lit, built on the [Navigation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API).
|
|
4
|
+
|
|
5
|
+
Fork of [`@lit-labs/router`](https://github.com/lit/lit/tree/main/packages/labs/router)
|
|
6
|
+
— see [NOTICE.md](./NOTICE.md) for provenance, licence and the full list of
|
|
7
|
+
changes. Not affiliated with or endorsed by Google or the Lit team.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm i lit-navigation-router
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
Upstream commits navigation with `history.pushState()` and reacts to `popstate`,
|
|
16
|
+
but swaps the outlet only after awaiting `route.enter()`. The URL therefore
|
|
17
|
+
leads and the outlet lags, so the route being left re-renders with stale params
|
|
18
|
+
and two quick navigations can commit out of order.
|
|
19
|
+
|
|
20
|
+
`navigateEvent.intercept()` removes that: the browser commits the URL and holds
|
|
21
|
+
the navigation un-finished while the handler runs, and aborts
|
|
22
|
+
`navigateEvent.signal` when a newer navigation supersedes this one.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Identical to upstream:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import {Router} from 'lit-navigation-router/router.js';
|
|
30
|
+
|
|
31
|
+
class MyApp extends LitElement {
|
|
32
|
+
private _router = new Router(this, [
|
|
33
|
+
{path: '/', render: () => html`<h1>Home</h1>`},
|
|
34
|
+
{
|
|
35
|
+
path: '/item/:id',
|
|
36
|
+
enter: async () => {
|
|
37
|
+
await import('./item-view.js'); // awaited before the outlet swaps
|
|
38
|
+
return true;
|
|
39
|
+
},
|
|
40
|
+
render: ({id}) => html`<item-view .id=${id}></item-view>`,
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
render() {
|
|
45
|
+
return html`${this._router.outlet()}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Two additions:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// Forwarded to navigateEvent.intercept()
|
|
54
|
+
this._router.interceptOptions = {scroll: 'manual', focusReset: 'manual'};
|
|
55
|
+
|
|
56
|
+
// goto() takes an abort signal; Router passes navigateEvent.signal for you
|
|
57
|
+
await routes.goto('/item/1', {signal});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Browser support — please read
|
|
61
|
+
|
|
62
|
+
This router **requires the Navigation API**. There is no legacy fallback.
|
|
63
|
+
|
|
64
|
+
The API is Baseline **Newly** Available (January 2026: Chrome/Edge, Safari 26.2,
|
|
65
|
+
Firefox 147). Baseline **Widely** Available is not until roughly mid-2028, so
|
|
66
|
+
older engines are still in the wild.
|
|
67
|
+
|
|
68
|
+
On an engine without it, `Router` renders the current route on load but does not
|
|
69
|
+
intercept navigation — every link becomes an ordinary full page load. If your
|
|
70
|
+
server serves the app shell on every route that is slower, not broken. If it
|
|
71
|
+
does not, those links 404.
|
|
72
|
+
|
|
73
|
+
**One case is genuinely broken, not just slow.** If you navigate
|
|
74
|
+
programmatically with `history.pushState()` + `router.goto()`, nothing listens
|
|
75
|
+
for the resulting `popstate` — so Back moves the URL while the outlet stays put,
|
|
76
|
+
which is the URL/outlet split this fork exists to eliminate. Apps using that
|
|
77
|
+
pattern should gate on `supportsNavigationApi()` rather than accept the
|
|
78
|
+
degradation.
|
|
79
|
+
|
|
80
|
+
`supportsNavigationApi()` is exported so you can detect this at boot:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import {supportsNavigationApi} from 'lit-navigation-router/router.js';
|
|
84
|
+
|
|
85
|
+
if (!supportsNavigationApi()) {
|
|
86
|
+
showUpgradePrompt();
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
If you need real pre-2026 support, pair this with a Navigation API polyfill
|
|
91
|
+
rather than a second router: one decision path, with compatibility isolated in
|
|
92
|
+
a layer whose whole job is spec accuracy.
|
|
93
|
+
|
|
94
|
+
## `URLPattern`
|
|
95
|
+
|
|
96
|
+
Route patterns are compiled with `URLPattern`, which this package uses from the
|
|
97
|
+
global scope and does **not** polyfill — same as upstream. Every engine that has
|
|
98
|
+
the Navigation API also has `URLPattern`, so if the support check above passes
|
|
99
|
+
you need nothing. If you support older engines anyway, load
|
|
100
|
+
[`urlpattern-polyfill`](https://www.npmjs.com/package/urlpattern-polyfill)
|
|
101
|
+
before the router:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import {URLPattern} from 'urlpattern-polyfill';
|
|
105
|
+
if (!globalThis.URLPattern) {
|
|
106
|
+
(globalThis as {URLPattern?: unknown}).URLPattern = URLPattern;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The published types do not depend on it — `URLPatternRouteConfig.pattern` is
|
|
111
|
+
typed structurally, so a consumer build never needs the polyfill's types.
|
|
112
|
+
|
|
113
|
+
## Develop
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
npm install
|
|
117
|
+
npm run build # tsc → development/
|
|
118
|
+
npm test # Web Test Runner (Chromium via Playwright)
|
|
119
|
+
npm run check-types
|
|
120
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
*/
|
|
6
|
+
export * from './routes.js';
|
|
7
|
+
export { Router, supportsNavigationApi } from './router.js';
|
|
8
|
+
export type { InterceptOptions } from './router.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAC,MAAM,EAAE,qBAAqB,EAAC,MAAM,aAAa,CAAC;AAC1D,YAAY,EAAC,gBAAgB,EAAC,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAC,MAAM,EAAE,qBAAqB,EAAC,MAAM,aAAa,CAAC","sourcesContent":["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n */\nexport * from './routes.js';\nexport {Router, supportsNavigationApi} from './router.js';\nexport type {InterceptOptions} from './router.js';\n"]}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
*
|
|
6
|
+
* Modifications Copyright 2026 VanLandingham Labs, same license.
|
|
7
|
+
* Rebuilt on the Navigation API; see NOTICE.md.
|
|
8
|
+
*/
|
|
9
|
+
import { Routes } from './routes.js';
|
|
10
|
+
/** The subset of `NavigationInterceptOptions` this router forwards. */
|
|
11
|
+
export interface InterceptOptions {
|
|
12
|
+
focusReset?: 'after-transition' | 'manual';
|
|
13
|
+
scroll?: 'after-transition' | 'manual';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* True when the Navigation API is available — Baseline Newly Available since
|
|
17
|
+
* January 2026 (Chrome/Edge, Safari 26.2, Firefox 147).
|
|
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.
|
|
22
|
+
*/
|
|
23
|
+
export declare const supportsNavigationApi: () => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* A root-level router that intercepts navigation via the Navigation API.
|
|
26
|
+
*
|
|
27
|
+
* This class extends Routes so that it can also have a route configuration.
|
|
28
|
+
*
|
|
29
|
+
* There should only be one Router instance on a page, since the Router
|
|
30
|
+
* installs a global listener. Nested routes should be configured with the
|
|
31
|
+
* `Routes` class.
|
|
32
|
+
*
|
|
33
|
+
* ## Why the Navigation API
|
|
34
|
+
*
|
|
35
|
+
* Upstream intercepted navigation with a global click listener plus `popstate`
|
|
36
|
+
* and committed with `history.pushState()`. That is structurally racy:
|
|
37
|
+
* `pushState` is synchronous and `popstate` fires *after* the URL has already
|
|
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.
|
|
65
|
+
*/
|
|
66
|
+
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
|
+
*/
|
|
71
|
+
interceptOptions?: InterceptOptions;
|
|
72
|
+
private _listening;
|
|
73
|
+
hostConnected(): void;
|
|
74
|
+
hostDisconnected(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Handles same-document navigation from every source at once: anchor clicks,
|
|
77
|
+
* `navigation.navigate()`, `history.pushState()`, and back/forward.
|
|
78
|
+
*/
|
|
79
|
+
private _onNavigate;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +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;AAwBnC,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,CAmDjB;CACH"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
*
|
|
6
|
+
* Modifications Copyright 2026 VanLandingham Labs, same license.
|
|
7
|
+
* Rebuilt on the Navigation API; see NOTICE.md.
|
|
8
|
+
*/
|
|
9
|
+
import { Routes } from './routes.js';
|
|
10
|
+
// We cache the origin since it can't change
|
|
11
|
+
const origin = location.origin || location.protocol + '//' + location.host;
|
|
12
|
+
const getNavigation = () => window.navigation;
|
|
13
|
+
/**
|
|
14
|
+
* True when the Navigation API is available — Baseline Newly Available since
|
|
15
|
+
* January 2026 (Chrome/Edge, Safari 26.2, Firefox 147).
|
|
16
|
+
*
|
|
17
|
+
* This router **requires** it. Exported so an app can detect an unsupported
|
|
18
|
+
* engine at boot and say so, rather than leaving the user to notice that every
|
|
19
|
+
* link reloads the page.
|
|
20
|
+
*/
|
|
21
|
+
export const supportsNavigationApi = () => typeof window !== 'undefined' &&
|
|
22
|
+
typeof getNavigation()?.addEventListener === 'function';
|
|
23
|
+
/**
|
|
24
|
+
* A root-level router that intercepts navigation via the Navigation API.
|
|
25
|
+
*
|
|
26
|
+
* This class extends Routes so that it can also have a route configuration.
|
|
27
|
+
*
|
|
28
|
+
* There should only be one Router instance on a page, since the Router
|
|
29
|
+
* installs a global listener. Nested routes should be configured with the
|
|
30
|
+
* `Routes` class.
|
|
31
|
+
*
|
|
32
|
+
* ## Why the Navigation API
|
|
33
|
+
*
|
|
34
|
+
* Upstream intercepted navigation with a global click listener plus `popstate`
|
|
35
|
+
* and committed with `history.pushState()`. That is structurally racy:
|
|
36
|
+
* `pushState` is synchronous and `popstate` fires *after* the URL has already
|
|
37
|
+
* moved, but `goto()` awaits `route.enter()` before swapping the outlet. The
|
|
38
|
+
* URL leads and the outlet lags, leaving two sources of truth — the outgoing
|
|
39
|
+
* route re-renders with stale params, and two quick navigations commit in
|
|
40
|
+
* whatever order their `enter()` hooks happen to resolve.
|
|
41
|
+
*
|
|
42
|
+
* `navigateEvent.intercept({handler})` collapses that. The browser commits the
|
|
43
|
+
* URL and holds the navigation un-finished while the handler runs, and it
|
|
44
|
+
* aborts `navigateEvent.signal` when a newer navigation supersedes this one —
|
|
45
|
+
* which `goto()` honours, so a superseded route can no longer win the outlet.
|
|
46
|
+
*
|
|
47
|
+
* ## No legacy fallback
|
|
48
|
+
*
|
|
49
|
+
* An earlier version of this fork kept upstream's click/popstate path for
|
|
50
|
+
* pre-2026 engines. It was removed deliberately. Ten review rounds found
|
|
51
|
+
* divergences between the two paths and **every one was in the click handler**,
|
|
52
|
+
* never in this one — which is structural, not luck: this handler reads a
|
|
53
|
+
* decision the browser has already made, while the click handler had to
|
|
54
|
+
* re-derive it, re-implementing the rules for choosing a navigable, the
|
|
55
|
+
* fragment-navigation predicate, and the modifier-key rules. Each round found
|
|
56
|
+
* another place where the re-implementation and the spec disagreed.
|
|
57
|
+
*
|
|
58
|
+
* On an engine without the API, links fall back to ordinary full page loads.
|
|
59
|
+
* For an app whose server serves the shell on every route that still works —
|
|
60
|
+
* it is slower, not broken — and `supportsNavigationApi()` lets you detect it.
|
|
61
|
+
* If real pre-2026 support is ever needed, use a Navigation API polyfill: one
|
|
62
|
+
* decision path, with compatibility isolated in a layer whose whole job is
|
|
63
|
+
* spec accuracy.
|
|
64
|
+
*/
|
|
65
|
+
export class Router extends Routes {
|
|
66
|
+
/**
|
|
67
|
+
* Options forwarded to `navigateEvent.intercept()`. Leaving these unset
|
|
68
|
+
* gives the browser's default scroll and focus handling.
|
|
69
|
+
*/
|
|
70
|
+
interceptOptions;
|
|
71
|
+
_listening = false;
|
|
72
|
+
hostConnected() {
|
|
73
|
+
super.hostConnected();
|
|
74
|
+
// Gated on the exported predicate, not on `navigation !== undefined`:
|
|
75
|
+
// a stub or partial polyfill under that name would otherwise make this
|
|
76
|
+
// branch throw out of connectedCallback while `supportsNavigationApi()`
|
|
77
|
+
// told the app it was unsupported — and then even the initial render below
|
|
78
|
+
// would not run.
|
|
79
|
+
if (supportsNavigationApi()) {
|
|
80
|
+
getNavigation().addEventListener('navigate', this._onNavigate);
|
|
81
|
+
this._listening = true;
|
|
82
|
+
}
|
|
83
|
+
// Kick off routed rendering by going to the current URL. Done even without
|
|
84
|
+
// the API: a full page load still renders the right route, which is what
|
|
85
|
+
// makes the unsupported-engine degradation "slow" rather than "blank".
|
|
86
|
+
// Surfaced rather than left as a bare unhandled rejection, matching the
|
|
87
|
+
// convention in routes.ts: on an engine without the API this is the *only*
|
|
88
|
+
// rendering path, and a deep link with no matching route throws here.
|
|
89
|
+
void this.goto(window.location.pathname).catch((err) => {
|
|
90
|
+
queueMicrotask(() => {
|
|
91
|
+
throw err;
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
hostDisconnected() {
|
|
96
|
+
super.hostDisconnected();
|
|
97
|
+
if (this._listening) {
|
|
98
|
+
getNavigation()?.removeEventListener('navigate', this._onNavigate);
|
|
99
|
+
this._listening = false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Handles same-document navigation from every source at once: anchor clicks,
|
|
104
|
+
* `navigation.navigate()`, `history.pushState()`, and back/forward.
|
|
105
|
+
*/
|
|
106
|
+
_onNavigate = (e) => {
|
|
107
|
+
// Not ours to handle: anything the browser says cannot be intercepted,
|
|
108
|
+
// fragment-only moves, downloads, and POST form submissions.
|
|
109
|
+
if (!e.canIntercept || e.hashChange || e.downloadRequest !== null) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (e.formData) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Reloads must stay reloads. `canIntercept` is true for them, so without
|
|
116
|
+
// this `location.reload()` silently degrades to re-running goto() on the
|
|
117
|
+
// same path — the document is never replaced, breaking the standard
|
|
118
|
+
// "new version available, reload" escape hatch. (It would also disagree
|
|
119
|
+
// with the browser's own refresh button, which is not interceptable.)
|
|
120
|
+
if (e.navigationType === 'reload') {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// `rel="external"` is a convention this router honours — it is not defined
|
|
124
|
+
// by HTML or by the Navigation API, so the browser will not decline these
|
|
125
|
+
// for us. Best-effort: `sourceElement` is not in every engine, and is
|
|
126
|
+
// absent for programmatic navigation.
|
|
127
|
+
if (e.sourceElement?.getAttribute?.('rel') === 'external') {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const url = new URL(e.destination.url);
|
|
131
|
+
if (url.origin !== origin) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Only intercept what we can actually render. `canIntercept` is true for
|
|
135
|
+
// any same-origin URL, including cross-document ones — so without this a
|
|
136
|
+
// link to a server-rendered page, an export endpoint, or a GET form
|
|
137
|
+
// (whose `formData` is null) gets swallowed: the URL commits, goto()
|
|
138
|
+
// throws "No route found", and the address bar is left pointing somewhere
|
|
139
|
+
// the outlet never went. Declining lets the browser do the real
|
|
140
|
+
// navigation, which is the correct outcome.
|
|
141
|
+
if (!this.hasRouteFor(url.pathname)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
e.intercept({
|
|
145
|
+
...this.interceptOptions,
|
|
146
|
+
handler: async () => {
|
|
147
|
+
// `e.signal` aborts if another navigation starts before this handler
|
|
148
|
+
// resolves; goto() checks it after `enter()` and stands down.
|
|
149
|
+
await this.goto(url.pathname, { signal: e.signal });
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +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// We cache the origin since it can't change\nconst origin = location.origin || location.protocol + '//' + location.host;\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 if (!e.canIntercept || e.hashChange || e.downloadRequest !== null) {\n return;\n }\n if (e.formData) {\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 const url = new URL(e.destination.url);\n if (url.origin !== 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"]}
|