lit-navigation-router 0.3.0 → 0.4.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 +45 -0
- package/NOTICE.md +21 -5
- package/README.md +28 -0
- package/development/routes.d.ts +14 -2
- package/development/routes.d.ts.map +1 -1
- package/development/routes.js +71 -61
- package/development/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/routes.ts +86 -65
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A route's tail is identified from its pattern, not guessed from the
|
|
8
|
+
match's groups object**
|
|
9
|
+
([#4](https://github.com/VanLandinghamLabs/lit-router/issues/4)).
|
|
10
|
+
`URLPattern` keys every unnamed group by position, so an unnamed regex group
|
|
11
|
+
(`/post/(\d+)`) and a wildcard that is not last (`/foo/*/bar`) looked exactly
|
|
12
|
+
like a trailing `/*`. Both were handed to child controllers as a tail and
|
|
13
|
+
stripped from `link()`, which returned `/post/` for `/post/123` and a
|
|
14
|
+
truncated `/foo/zz/b` for `/foo/zz/bar`. Only a pattern that ends in a
|
|
15
|
+
wildcard now has a tail.
|
|
16
|
+
- **A nested `fallback` passes its tail on to its own children**
|
|
17
|
+
([#5](https://github.com/VanLandinghamLabs/lit-router/issues/5)). The
|
|
18
|
+
fallback matched with a literal `/*` pattern, but a nested controller is
|
|
19
|
+
handed its tail without a leading slash, which `/*` rejects: the fallback
|
|
20
|
+
rendered with empty params and grandchildren were never routed or
|
|
21
|
+
superseded. It now behaves like `/*` at the root and `*` when nested, with
|
|
22
|
+
`params[0]` the whole tail in both cases.
|
|
23
|
+
- **Children are superseded when the parent moves to a route with no tail**
|
|
24
|
+
([#7](https://github.com/VanLandinghamLabs/lit-router/issues/7)). The
|
|
25
|
+
propagation loop ran only when the new route had a tail, so a child
|
|
26
|
+
mid-`enter()` for the previous tail was never stood down and could commit
|
|
27
|
+
over a URL that had already moved on.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- `URLPatternLike` requires `pathname`: the pattern string, which a real
|
|
32
|
+
`URLPattern` exposes and which is how the tail is now identified. An object
|
|
33
|
+
offering only `test()`/`exec()` no longer type-checks as a route pattern.
|
|
34
|
+
- A trailing `*` counts as a tail only when it is a wildcard: bare `*`, `{*}`,
|
|
35
|
+
`(.*)` (which `URLPattern` normalises to `*`), optionally followed by `?`.
|
|
36
|
+
A `*` that is the modifier on a group or a named param (`(\d+)*`, `{/}*`,
|
|
37
|
+
`:rest*`), or an escaped `\*`, is not. Previously any pattern whose match
|
|
38
|
+
produced a positional group was treated as having a tail.
|
|
39
|
+
|
|
40
|
+
### Documentation
|
|
41
|
+
|
|
42
|
+
- A nested index route is spelled `{path: ''}`
|
|
43
|
+
([#6](https://github.com/VanLandinghamLabs/lit-router/issues/6)). The tail
|
|
44
|
+
handed to a child has no leading slash, so the index of a nested route space
|
|
45
|
+
is the empty string; `{path: '/'}` matches nothing there. This already
|
|
46
|
+
worked and is now documented and pinned by a test.
|
|
47
|
+
|
|
3
48
|
## 0.3.0
|
|
4
49
|
|
|
5
50
|
### 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
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
-
|
|
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` →
|
|
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
|
@@ -57,6 +57,34 @@ this._router.interceptOptions = {scroll: 'manual', focusReset: 'manual'};
|
|
|
57
57
|
await routes.goto('/item/1', {signal});
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
## Nested routes and tails
|
|
61
|
+
|
|
62
|
+
A route whose pattern ends in a wildcard — `/docs/*` — hands what the wildcard
|
|
63
|
+
matched (the *tail*) to any `Routes` controller mounted by its `render()`. The
|
|
64
|
+
tail has no leading slash, and child routes are written the same way:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
// Parent
|
|
68
|
+
{path: '/docs/*', render: () => html`<my-docs></my-docs>`}
|
|
69
|
+
|
|
70
|
+
// Child, inside <my-docs>
|
|
71
|
+
private _routes = new Routes(this, [
|
|
72
|
+
{path: '', render: () => html`<h2>Docs</h2>`}, // /docs/
|
|
73
|
+
{path: ':page', render: ({page}) => html`<doc-page .page=${page}></doc-page>`}, // /docs/intro
|
|
74
|
+
]);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The index of a nested route space is the empty tail, spelled `{path: ''}`.
|
|
78
|
+
`{path: '/'}` matches nothing there: it would need a tail of `/`, i.e. a URL of
|
|
79
|
+
`/docs//`.
|
|
80
|
+
|
|
81
|
+
Only a trailing wildcard produces a tail. An unnamed regex group
|
|
82
|
+
(`/post/(\d+)`) or a wildcard followed by more pattern (`/a/*/b`) is a
|
|
83
|
+
parameter of that route, available as `params[0]`; it is neither passed to
|
|
84
|
+
children nor stripped from `link()`. A `fallback` behaves like a `/*` route and
|
|
85
|
+
passes the whole pathname on as the tail. Nested, it accepts the slash-less
|
|
86
|
+
tail it is handed and passes that on.
|
|
87
|
+
|
|
60
88
|
## Browser support — please read
|
|
61
89
|
|
|
62
90
|
This router **requires the Navigation API**. There is no legacy fallback.
|
package/development/routes.d.ts
CHANGED
|
@@ -44,6 +44,12 @@ 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
|
+
/**
|
|
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;
|
|
47
53
|
test(input: {
|
|
48
54
|
pathname: string;
|
|
49
55
|
}): boolean;
|
|
@@ -71,7 +77,10 @@ export declare class Routes implements ReactiveController {
|
|
|
71
77
|
routes: Array<RouteConfig>;
|
|
72
78
|
/**
|
|
73
79
|
* A default fallback route which will always be matched if none of the
|
|
74
|
-
* {@link routes} match.
|
|
80
|
+
* {@link routes} match. Behaves like a `/*` route: `params[0]` is the whole
|
|
81
|
+
* pathname minus its leading slash, and is handed to child controllers as
|
|
82
|
+
* their tail. A nested controller's own pathname is a tail, with no leading
|
|
83
|
+
* slash; the fallback accepts that too.
|
|
75
84
|
*/
|
|
76
85
|
fallback?: BaseRouteConfig;
|
|
77
86
|
private readonly _childRoutes;
|
|
@@ -79,6 +88,7 @@ export declare class Routes implements ReactiveController {
|
|
|
79
88
|
/** Monotonic goto counter; see the last-goto-wins note in goto(). */
|
|
80
89
|
private _gotoSeq;
|
|
81
90
|
private _currentPathname;
|
|
91
|
+
private _currentTail;
|
|
82
92
|
private _currentRoute;
|
|
83
93
|
private _currentParams;
|
|
84
94
|
/**
|
|
@@ -133,7 +143,9 @@ export declare class Routes implements ReactiveController {
|
|
|
133
143
|
* a genuine `enter()` rejection still surfaces the way it does upstream.
|
|
134
144
|
* Skipping must still supersede: `goto()` is where the counter is bumped, so
|
|
135
145
|
* returning without it would leave an in-flight child navigation current,
|
|
136
|
-
* free to commit over a URL that has moved on.
|
|
146
|
+
* free to commit over a URL that has moved on. A parent route with no tail
|
|
147
|
+
* at all is the same case: nothing to route, but still something to stand
|
|
148
|
+
* down.
|
|
137
149
|
*
|
|
138
150
|
* No abort signal is threaded through, and the goto is deliberately not
|
|
139
151
|
* awaited. The parent commits its own state before children run, so a child
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAC,kBAAkB,EAAE,sBAAsB,EAAC,MAAM,KAAK,CAAC;AAEpE,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAC,KAAK,OAAO,CAAC;IAClE,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE;QACf,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KACnC,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAsB,SAAQ,eAAe;IAC5D,OAAO,EAAE,cAAc,CAAC;CACzB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,KAAK,EAAE;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,GAAG,OAAO,CAAC;IACzC,IAAI,CAAC,KAAK,EAAE;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAC,GAAG;QAC/B,QAAQ,EAAE;YAAC,MAAM,EAAE;gBAAC,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;aAAC,CAAA;SAAC,CAAC;KACzD,GAAG,IAAI,CAAC;CACV;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
package/development/routes.js
CHANGED
|
@@ -21,11 +21,42 @@ const getPattern = (route) => {
|
|
|
21
21
|
}
|
|
22
22
|
return pattern;
|
|
23
23
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Matches a pathname pattern that ends in a wildcard, in the forms
|
|
26
|
+
* `URLPattern.prototype.pathname` regenerates one: a bare `*` — which a
|
|
27
|
+
* trailing `(.*)` also normalises to — or `{*}`, which the generator emits for
|
|
28
|
+
* a wildcard after a modified group, e.g. `/docs{/}?*`. Either may be
|
|
29
|
+
* optional (`*?`). Not a wildcard: an escaped `\*`, or a `*` that is the
|
|
30
|
+
* modifier on a group (`(\d+)*`, `{/}*`) or on a named param (`:rest*`).
|
|
31
|
+
* Those exclusions matter when an earlier positional group exists —
|
|
32
|
+
* `/x/(\d+)/:rest*` — since that group would otherwise be taken for the tail.
|
|
33
|
+
*/
|
|
34
|
+
const TRAILING_WILDCARD = /(?:(?<![\\)}]|:[\w$]+)\*|\{\*\})\??$/;
|
|
35
|
+
/**
|
|
36
|
+
* The tail of a match — what a trailing wildcard (`/foo/*`) captured — or
|
|
37
|
+
* undefined when the pattern has none.
|
|
38
|
+
*
|
|
39
|
+
* Decided from the pattern, not from the groups object: an unnamed regex group
|
|
40
|
+
* (`/post/(\d+)`) and a wildcard that is not last (`/foo/*` followed by
|
|
41
|
+
* `/bar`) are keyed by index exactly as a tail is, and reading either as one
|
|
42
|
+
* truncated `link()` and handed a child the wrong segment. When a trailing
|
|
43
|
+
* wildcard is present it is the last group in the pattern, so its key is the
|
|
44
|
+
* highest positional index.
|
|
45
|
+
*/
|
|
46
|
+
const tailOf = (route, params) => {
|
|
47
|
+
if (!TRAILING_WILDCARD.test(getPattern(route).pathname)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
let tailIndex = -1;
|
|
51
|
+
for (const key of Object.keys(params)) {
|
|
52
|
+
// Numeric, not lexicographic: '9' sorts above '10' as a string, so a
|
|
53
|
+
// pattern with eleven or more wildcards picked group 9 as its tail.
|
|
54
|
+
if (/^\d+$/.test(key) && Number(key) > tailIndex) {
|
|
55
|
+
tailIndex = Number(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return tailIndex < 0 ? undefined : params[String(tailIndex)];
|
|
59
|
+
};
|
|
29
60
|
/**
|
|
30
61
|
* A reactive controller that performs location-based routing using a
|
|
31
62
|
* configuration of URL patterns and associated render callbacks.
|
|
@@ -51,7 +82,10 @@ export class Routes {
|
|
|
51
82
|
routes = [];
|
|
52
83
|
/**
|
|
53
84
|
* A default fallback route which will always be matched if none of the
|
|
54
|
-
* {@link routes} match.
|
|
85
|
+
* {@link routes} match. Behaves like a `/*` route: `params[0]` is the whole
|
|
86
|
+
* pathname minus its leading slash, and is handed to child controllers as
|
|
87
|
+
* their tail. A nested controller's own pathname is a tail, with no leading
|
|
88
|
+
* slash; the fallback accepts that too.
|
|
55
89
|
*/
|
|
56
90
|
fallback;
|
|
57
91
|
/*
|
|
@@ -70,6 +104,7 @@ export class Routes {
|
|
|
70
104
|
/** Monotonic goto counter; see the last-goto-wins note in goto(). */
|
|
71
105
|
_gotoSeq = 0;
|
|
72
106
|
_currentPathname;
|
|
107
|
+
_currentTail;
|
|
73
108
|
_currentRoute;
|
|
74
109
|
_currentParams = {};
|
|
75
110
|
/**
|
|
@@ -127,15 +162,15 @@ export class Routes {
|
|
|
127
162
|
// newer one. This also keeps `Routes` correct when used on its own, with
|
|
128
163
|
// no `Router` and no Navigation API in the picture.
|
|
129
164
|
const seq = ++this._gotoSeq;
|
|
130
|
-
let
|
|
165
|
+
let tail;
|
|
131
166
|
if (this.routes.length === 0 && this.fallback === undefined) {
|
|
132
167
|
// If a routes controller has none of its own routes it acts like it has
|
|
133
168
|
// one route of `/*` so that it passes the whole pathname as a tail
|
|
134
169
|
// match.
|
|
135
|
-
|
|
170
|
+
tail = pathname;
|
|
136
171
|
this._currentPathname = '';
|
|
137
172
|
// Simulate a tail group with the whole pathname
|
|
138
|
-
this._currentParams = { 0:
|
|
173
|
+
this._currentParams = { 0: tail };
|
|
139
174
|
}
|
|
140
175
|
else {
|
|
141
176
|
const match = this._match(pathname);
|
|
@@ -143,7 +178,7 @@ export class Routes {
|
|
|
143
178
|
throw new Error(`No route found for ${pathname}`);
|
|
144
179
|
}
|
|
145
180
|
const { route, params } = match;
|
|
146
|
-
|
|
181
|
+
tail = match.tail;
|
|
147
182
|
if (typeof route.enter === 'function') {
|
|
148
183
|
const success = await route.enter(params);
|
|
149
184
|
// If enter() returns false, cancel this navigation
|
|
@@ -160,10 +195,11 @@ export class Routes {
|
|
|
160
195
|
this._currentRoute = route;
|
|
161
196
|
this._currentParams = params;
|
|
162
197
|
this._currentPathname =
|
|
163
|
-
|
|
198
|
+
tail === undefined
|
|
164
199
|
? pathname
|
|
165
|
-
: pathname.substring(0, pathname.length -
|
|
200
|
+
: pathname.substring(0, pathname.length - tail.length);
|
|
166
201
|
}
|
|
202
|
+
this._currentTail = tail;
|
|
167
203
|
// Propagate the tail match to children — deliberately NOT awaited.
|
|
168
204
|
//
|
|
169
205
|
// Awaiting looks like it would make `navigation.finished` cover the whole
|
|
@@ -177,10 +213,13 @@ export class Routes {
|
|
|
177
213
|
// committed, outlet stranded, i.e. this fork's own thesis bug one level
|
|
178
214
|
// down. Nested supersession is handled by the goto counter above, not by
|
|
179
215
|
// awaiting. `_routeChild` covers the per-child filtering and error policy.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
216
|
+
//
|
|
217
|
+
// Runs whether or not there is a tail. A route without one has nothing for
|
|
218
|
+
// the children to render, but they must still be superseded — otherwise a
|
|
219
|
+
// child mid-`enter()` for the previous tail stays current and commits over
|
|
220
|
+
// a URL that has moved on.
|
|
221
|
+
for (const childRoutes of this._childRoutes) {
|
|
222
|
+
this._routeChild(childRoutes, tail);
|
|
184
223
|
}
|
|
185
224
|
this._host.requestUpdate();
|
|
186
225
|
}
|
|
@@ -207,7 +246,9 @@ export class Routes {
|
|
|
207
246
|
* a genuine `enter()` rejection still surfaces the way it does upstream.
|
|
208
247
|
* Skipping must still supersede: `goto()` is where the counter is bumped, so
|
|
209
248
|
* returning without it would leave an in-flight child navigation current,
|
|
210
|
-
* free to commit over a URL that has moved on.
|
|
249
|
+
* free to commit over a URL that has moved on. A parent route with no tail
|
|
250
|
+
* at all is the same case: nothing to route, but still something to stand
|
|
251
|
+
* down.
|
|
211
252
|
*
|
|
212
253
|
* No abort signal is threaded through, and the goto is deliberately not
|
|
213
254
|
* awaited. The parent commits its own state before children run, so a child
|
|
@@ -217,7 +258,7 @@ export class Routes {
|
|
|
217
258
|
* replacement. Supersession is the counter's job.
|
|
218
259
|
*/
|
|
219
260
|
_routeChild(child, tail) {
|
|
220
|
-
if (!child.hasRouteFor(tail)) {
|
|
261
|
+
if (tail === undefined || !child.hasRouteFor(tail)) {
|
|
221
262
|
child._supersede();
|
|
222
263
|
return;
|
|
223
264
|
}
|
|
@@ -284,22 +325,21 @@ export class Routes {
|
|
|
284
325
|
for (const route of this.routes) {
|
|
285
326
|
const result = getPattern(route).exec({ pathname });
|
|
286
327
|
if (result !== null) {
|
|
287
|
-
|
|
328
|
+
const params = result.pathname.groups;
|
|
329
|
+
return { route, params, tail: tailOf(route, params) };
|
|
288
330
|
}
|
|
289
331
|
}
|
|
290
332
|
if (this.fallback === undefined) {
|
|
291
333
|
return undefined;
|
|
292
334
|
}
|
|
293
335
|
// The fallback route behaves like it has a "/*" path. This is hidden from
|
|
294
|
-
// the public API
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
params: wildcard.exec({ pathname })?.pathname.groups ?? {},
|
|
302
|
-
};
|
|
336
|
+
// the public API; the `path` is there to return a valid RouteConfig. The
|
|
337
|
+
// match itself is done by hand rather than with a real `/*` pattern: a
|
|
338
|
+
// nested controller is handed its tail *without* a leading slash, which
|
|
339
|
+
// `/*` does not match, so a nested fallback matched nothing — empty
|
|
340
|
+
// params, no tail, and its own children never routed.
|
|
341
|
+
const tail = pathname.startsWith('/') ? pathname.slice(1) : pathname;
|
|
342
|
+
return { route: { ...this.fallback, path: '/*' }, params: { 0: tail }, tail };
|
|
303
343
|
}
|
|
304
344
|
hostConnected() {
|
|
305
345
|
this._host.addEventListener(RoutesConnectedEvent.eventName, this._onRoutesConnected);
|
|
@@ -339,41 +379,11 @@ export class Routes {
|
|
|
339
379
|
};
|
|
340
380
|
// A child that mounts under an existing tail match has to be caught up to
|
|
341
381
|
// it — it missed the propagation loop in goto() that ran before it existed.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
382
|
+
// With no tail there is nothing to catch up to, and `_routeChild` then only
|
|
383
|
+
// supersedes, a no-op on a freshly mounted child.
|
|
384
|
+
this._routeChild(childRoutes, this._currentTail);
|
|
346
385
|
};
|
|
347
386
|
}
|
|
348
|
-
/**
|
|
349
|
-
* Returns the tail of a pathname groups object. This is the match from a
|
|
350
|
-
* wildcard at the end of a pathname pattern, like `/foo/*`
|
|
351
|
-
*/
|
|
352
|
-
const getTailGroup = (groups) => {
|
|
353
|
-
let tailIndex = -1;
|
|
354
|
-
for (const key of Object.keys(groups)) {
|
|
355
|
-
// Anchored. `URLPattern` keys a positional group by its index, so a
|
|
356
|
-
// non-digit key is never one — an unanchored test also accepts a *named*
|
|
357
|
-
// group containing a digit (`:id2`), and since a letter sorts above a
|
|
358
|
-
// digit it then won the comparison below and the param value was handed
|
|
359
|
-
// to the child instead of the tail.
|
|
360
|
-
//
|
|
361
|
-
// Necessary, not sufficient: an unnamed *regex* group is positional too
|
|
362
|
-
// (`/post/(\d+)` yields key "0"), as is a wildcard that is not last
|
|
363
|
-
// (`/foo/*/bar`). Both are mis-read as tails here, and both predate this
|
|
364
|
-
// check — selecting the tail properly needs the pattern, not just groups.
|
|
365
|
-
if (!/^\d+$/.test(key)) {
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
// Numeric, not lexicographic: '9' sorts above '10' as a string, so a
|
|
369
|
-
// pattern with eleven or more wildcards picked group 9 as its tail.
|
|
370
|
-
const index = Number(key);
|
|
371
|
-
if (index > tailIndex) {
|
|
372
|
-
tailIndex = index;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
return tailIndex < 0 ? undefined : groups[String(tailIndex)];
|
|
376
|
-
};
|
|
377
387
|
/**
|
|
378
388
|
* This event is fired from Routes controllers when their host is connected to
|
|
379
389
|
* announce the child route and potentially connect to a parent routes controller.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Modifications Copyright 2026 VanLandingham Labs, same license. See NOTICE.md.\n */\n\n/// <reference types=\"urlpattern-polyfill\" />\n\nimport type {ReactiveController, ReactiveControllerHost} from 'lit';\n\nexport interface BaseRouteConfig {\n name?: string | undefined;\n render?: (params: {[key: string]: string | undefined}) => unknown;\n enter?: (params: {\n [key: string]: string | undefined;\n }) => Promise<boolean> | boolean;\n}\n\n/**\n * A RouteConfig that matches against a `path` string. `path` must be a\n * [`URLPattern` compatible pathname pattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/pathname).\n */\nexport interface PathRouteConfig extends BaseRouteConfig {\n path: string;\n}\n\n/**\n * A RouteConfig that matches against a given [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)\n *\n * While `URLPattern` can match against protocols, hostnames, and ports,\n * routes will only be checked for matches if they're part of the current\n * origin. This means that the pattern is limited to checking `pathname` and\n * `search`.\n */\nexport interface URLPatternRouteConfig extends BaseRouteConfig {\n pattern: URLPatternLike;\n}\n\n/**\n * The part of `URLPattern` this router uses.\n *\n * Declared structurally rather than referencing the global so the emitted\n * `.d.ts` is self-contained: the `/// <reference types=\"urlpattern-polyfill\" />`\n * above is not carried into declaration output, and `URLPattern` is not in\n * TypeScript's bundled `lib.dom`, so a published package typed against the\n * global fails a consumer build with `TS2304: Cannot find name 'URLPattern'`.\n * A real `URLPattern` satisfies this, so passing one still type-checks.\n */\nexport interface URLPatternLike {\n test(input: {pathname: string}): boolean;\n exec(input: {pathname: string}): {\n pathname: {groups: {[key: string]: string | undefined}};\n } | null;\n}\n\n/**\n * A description of a route, which path or pattern to match against, and a\n * render() callback used to render a match to the outlet.\n */\nexport type RouteConfig = PathRouteConfig | URLPatternRouteConfig;\n\n// A cache of URLPatterns created for PathRouteConfig.\n// Rather than converting all given RoutConfigs to URLPatternRouteConfig, this\n// lets us make `routes` mutable so users can add new PathRouteConfigs\n// dynamically.\nconst patternCache = new WeakMap<PathRouteConfig, URLPatternLike>();\n\nconst isPatternConfig = (route: RouteConfig): route is URLPatternRouteConfig =>\n (route as URLPatternRouteConfig).pattern !== undefined;\n\nconst getPattern = (route: RouteConfig): URLPatternLike => {\n if (isPatternConfig(route)) {\n return route.pattern;\n }\n let pattern = patternCache.get(route);\n if (pattern === undefined) {\n patternCache.set(route, (pattern = new URLPattern({pathname: route.path})));\n }\n return pattern;\n};\n\n// The implicit \"/*\" pattern every configured fallback matches against. Built on\n// first use rather than at module scope so that importing this module never\n// touches `URLPattern` — it may be polyfilled after import, or absent entirely.\nlet wildcardPattern: URLPatternLike | undefined;\n\nconst getWildcardPattern = (): URLPatternLike =>\n (wildcardPattern ??= new URLPattern({pathname: '/*'}));\n\n/**\n * A reactive controller that performs location-based routing using a\n * configuration of URL patterns and associated render callbacks.\n */\nexport class Routes implements ReactiveController {\n private readonly _host: ReactiveControllerHost & HTMLElement;\n\n /*\n * The currently installed set of routes in precedence order.\n *\n * This array is mutable. To dynamically add a new route you can write:\n *\n * ```ts\n * this._routes.routes.push({\n * path: '/foo',\n * render: () => html`<p>Foo</p>`,\n * });\n * ```\n *\n * Mutating this property does not trigger any route transitions. If the\n * changes may result is a different route matching for the current path, you\n * must instigate a route update with `goto()`.\n */\n routes: Array<RouteConfig> = [];\n\n /**\n * A default fallback route which will always be matched if none of the\n * {@link routes} match. Implicitly matches to the path \"/*\".\n */\n fallback?: BaseRouteConfig;\n\n /*\n * The current set of child Routes controllers. These are connected via\n * the routes-connected event.\n */\n private readonly _childRoutes: Array<Routes> = [];\n\n private _parentRoutes: Routes | undefined;\n\n /*\n * State related to the current matching route.\n *\n * We keep this so that consuming code can access current parameters, and so\n * that we can propagate tail matches to child routes if they are added after\n * navigation / matching.\n */\n /** Monotonic goto counter; see the last-goto-wins note in goto(). */\n private _gotoSeq = 0;\n\n private _currentPathname: string | undefined;\n private _currentRoute: RouteConfig | undefined;\n private _currentParams: {\n [key: string]: string | undefined;\n } = {};\n\n /**\n * Callback to call when this controller is disconnected.\n *\n * It's critical to call this immediately in hostDisconnected so that this\n * controller instance doesn't receive a tail match meant for another route.\n */\n // TODO (justinfagnani): Do we need this now that we have a direct reference\n // to the parent? We can call `this._parentRoutes.disconnect(this)`.\n private _onDisconnect: (() => void) | undefined;\n\n constructor(\n host: ReactiveControllerHost & HTMLElement,\n routes: Array<RouteConfig>,\n options?: {fallback?: BaseRouteConfig}\n ) {\n (this._host = host).addController(this);\n this.routes = [...routes];\n this.fallback = options?.fallback;\n }\n\n /**\n * Returns a URL string of the current route, including parent routes,\n * optionally replacing the local path with `pathname`.\n */\n link(pathname?: string): string {\n if (pathname?.startsWith('/')) {\n return pathname;\n }\n if (pathname?.startsWith('.')) {\n throw new Error('Not implemented');\n }\n pathname ??= this._currentPathname;\n return (this._parentRoutes?.link() ?? '') + pathname;\n }\n\n /**\n * Navigates this routes controller to `pathname`.\n *\n * This does not navigate parent routes, so it isn't (yet) a general page\n * navigation API. It does navigate child routes if pathname matches a\n * pattern with a tail wildcard pattern (`/*`).\n *\n * Pass `options.signal` to make the navigation abandonable. `enter()` is\n * awaited, so a second `goto()` can start — and finish — while the first is\n * still resolving its route; without a signal the slower one commits last\n * and the outlet ends up on a route the URL has already left. `Router`\n * threads `NavigateEvent.signal` through for exactly this reason.\n */\n async goto(pathname: string, options?: {signal?: AbortSignal}) {\n // TODO (justinfagnani): handle absolute vs relative paths separately.\n\n // TODO (justinfagnani): generalize this to handle query params and\n // fragments. It currently only handles path names because it's easier to\n // completely disregard the origin for now. The click handler only does\n // an in-page navigation if the origin matches anyway.\n // Last-goto-wins, per controller. The navigation signal alone is not\n // enough: a child controller mounts as a *result* of its parent's render,\n // so its first goto() comes from `_onRoutesConnected` — after the parent's\n // navigation has already finished, and therefore with a signal that will\n // never abort. Without this counter a slow first child load commits over a\n // newer one. This also keeps `Routes` correct when used on its own, with\n // no `Router` and no Navigation API in the picture.\n const seq = ++this._gotoSeq;\n let tailGroup: string | undefined;\n\n if (this.routes.length === 0 && this.fallback === undefined) {\n // If a routes controller has none of its own routes it acts like it has\n // one route of `/*` so that it passes the whole pathname as a tail\n // match.\n tailGroup = pathname;\n this._currentPathname = '';\n // Simulate a tail group with the whole pathname\n this._currentParams = {0: tailGroup};\n } else {\n const match = this._match(pathname);\n if (match === undefined) {\n throw new Error(`No route found for ${pathname}`);\n }\n const {route, params} = match;\n tailGroup = getTailGroup(params);\n if (typeof route.enter === 'function') {\n const success = await route.enter(params);\n // If enter() returns false, cancel this navigation\n if (success === false) {\n return;\n }\n }\n // A newer navigation superseded this one while `enter` was awaiting.\n // Committing now would swap the outlet onto a route the URL has left.\n if (options?.signal?.aborted === true || seq !== this._gotoSeq) {\n return;\n }\n // Only update route state if the enter handler completes successfully\n this._currentRoute = route;\n this._currentParams = params;\n this._currentPathname =\n tailGroup === undefined\n ? pathname\n : pathname.substring(0, pathname.length - tailGroup.length);\n }\n\n // Propagate the tail match to children — deliberately NOT awaited.\n //\n // Awaiting looks like it would make `navigation.finished` cover the whole\n // tree, and an earlier revision of this fork did it. It is wrong twice\n // over. At this point `requestUpdate()` has not run, so `_childRoutes`\n // still holds the *outgoing* branch's controller: awaiting it gates the\n // parent's outlet swap on an `enter()` for a tail that controller will\n // never render (a hung one blocks the navigation forever), and if that\n // child has no route for the new tail its `No route found` throw\n // propagates out of here and `requestUpdate()` below never runs — URL\n // committed, outlet stranded, i.e. this fork's own thesis bug one level\n // down. Nested supersession is handled by the goto counter above, not by\n // awaiting. `_routeChild` covers the per-child filtering and error policy.\n if (tailGroup !== undefined) {\n for (const childRoutes of this._childRoutes) {\n this._routeChild(childRoutes, tailGroup);\n }\n }\n this._host.requestUpdate();\n }\n\n /**\n * The result of calling the current route's render() callback.\n */\n outlet() {\n return this._currentRoute?.render?.(this._currentParams);\n }\n\n /**\n * The current parsed route parameters.\n */\n get params() {\n return this._currentParams;\n }\n\n /**\n * Hands a tail match to a child controller. Shared by the propagation loop in\n * `goto()` and the late-mount path in `_onRoutesConnected`, so that identical\n * input cannot be silent on one and an uncaught global throw on the other.\n *\n * A child with no route for the new tail is the expected case, not an error —\n * the outgoing branch mid-swap, or a deep link to a path the child cannot\n * render. Filtered structurally rather than by swallowing every rejection, so\n * a genuine `enter()` rejection still surfaces the way it does upstream.\n * Skipping must still supersede: `goto()` is where the counter is bumped, so\n * returning without it would leave an in-flight child navigation current,\n * free to commit over a URL that has moved on.\n *\n * No abort signal is threaded through, and the goto is deliberately not\n * awaited. The parent commits its own state before children run, so a child\n * handed an already-aborted signal stands down with no newer goto() arriving\n * to correct it, leaving the nested outlet stuck — reachable, because a\n * hash-only navigation aborts the outstanding one without producing a\n * replacement. Supersession is the counter's job.\n */\n private _routeChild(child: Routes, tail: string) {\n if (!child.hasRouteFor(tail)) {\n child._supersede();\n return;\n }\n void child.goto(tail).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n }\n\n /**\n * Invalidate any in-flight `goto()` on this controller without starting a\n * new one. Same-class access, so `_gotoSeq` stays private to `Routes`.\n */\n private _supersede(seen: Set<Routes> = new Set()): void {\n // Unreachable defence in depth. Upstream *can* produce a `_childRoutes`\n // cycle — a host carrying two Routes controllers, disconnected and\n // reconnected, ends up with each registered as the other's child — but\n // `hostDisconnected` below removes the listener that causes it, and a test\n // asserts the cycle cannot form. Kept because an unguarded recursive walk\n // over a cycle is a stack overflow rather than a misrender.\n if (seen.has(this)) {\n return;\n }\n seen.add(this);\n this._gotoSeq++;\n // Recursive: on the navigating branch the child's own propagation loop\n // reaches the grandchildren, but a skipped child never runs one — so\n // without this an in-flight grandchild `enter()` stays current and commits\n // over a URL that has moved on, the same defect one level deeper.\n for (const child of this._childRoutes) {\n child._supersede(seen);\n }\n }\n\n /**\n * True when this controller can render `pathname` — i.e. a route matches, or\n * a fallback is configured.\n *\n * `Router` gates interception on this: intercepting a path we cannot render\n * commits the URL and then throws out of `goto()`, leaving the address bar\n * moved and the outlet stale. Letting the browser handle it instead means a\n * server-rendered page, an export endpoint, or a GET form still works.\n */\n hasRouteFor(pathname: string): boolean {\n // A fallback matches everything, and a controller with no routes of its own\n // behaves as if it had a single `/*` route (goto()'s special case). Either\n // way the answer is yes without running a single pattern — worth\n // short-circuiting, since `Router` asks this on every navigation.\n if (this.fallback !== undefined || this.routes.length === 0) {\n return true;\n }\n // `test()`, not `_match()`: this only needs the yes/no, and `exec()` pays\n // ~8x on a hit to build a groups object the caller would throw away.\n return this.routes.some((r) => getPattern(r).test({pathname}));\n }\n\n /**\n * Matches `pathname` against the installed routes and returns the first match\n * with its parsed parameters, or the fallback's match if one is configured.\n *\n * One `exec()` per candidate rather than `test()` to select and `exec()` to\n * extract: that ran the winning pattern twice, and every caller that wants a\n * route wants its params too.\n */\n private _match(pathname: string):\n | {route: RouteConfig; params: {[key: string]: string | undefined}}\n | undefined {\n for (const route of this.routes) {\n const result = getPattern(route).exec({pathname});\n if (result !== null) {\n return {route, params: result.pathname.groups};\n }\n }\n if (this.fallback === undefined) {\n return undefined;\n }\n // The fallback route behaves like it has a \"/*\" path. This is hidden from\n // the public API but is added here to return a valid RouteConfig. The\n // pattern is the shared one rather than one derived from this object:\n // the spread produces a fresh object every call, which `patternCache` —\n // keyed by identity — would miss, rebuilding a URLPattern per navigation.\n const wildcard = getWildcardPattern();\n return {\n route: {...this.fallback, path: '/*'},\n params: wildcard.exec({pathname})?.pathname.groups ?? {},\n };\n }\n\n hostConnected() {\n this._host.addEventListener(\n RoutesConnectedEvent.eventName,\n this._onRoutesConnected\n );\n const event = new RoutesConnectedEvent(this);\n this._host.dispatchEvent(event);\n this._onDisconnect = event.onDisconnect;\n }\n\n hostDisconnected() {\n // Remove the listener hostConnected added. Without this a host that is\n // disconnected and reconnected (a repeat() reorder, a tab swap) leaves the\n // sibling controller's listener installed, so on the second connect it\n // claims the re-dispatching controller as *its* child and the pair point\n // at each other — a real `_childRoutes` cycle, which recursive walks turn\n // into a stack overflow.\n this._host.removeEventListener(\n RoutesConnectedEvent.eventName,\n this._onRoutesConnected\n );\n // When this child routes controller is disconnected because a parent\n // outlet rendered a different template, disconnecting will ensure that\n // this controller doesn't receive a tail match meant for another route.\n this._onDisconnect?.();\n this._parentRoutes = undefined;\n }\n\n private _onRoutesConnected = (e: RoutesConnectedEvent) => {\n // Don't handle the event fired by this routes controller, which we get\n // because we do this.dispatchEvent(...)\n if (e.routes === this) {\n return;\n }\n\n const childRoutes = e.routes;\n this._childRoutes.push(childRoutes);\n childRoutes._parentRoutes = this;\n\n e.stopImmediatePropagation();\n e.onDisconnect = () => {\n const index = this._childRoutes.indexOf(childRoutes);\n if (index !== -1) {\n this._childRoutes.splice(index, 1);\n }\n };\n\n // A child that mounts under an existing tail match has to be caught up to\n // it — it missed the propagation loop in goto() that ran before it existed.\n const tailGroup = getTailGroup(this._currentParams);\n if (tailGroup !== undefined) {\n this._routeChild(childRoutes, tailGroup);\n }\n };\n}\n\n/**\n * Returns the tail of a pathname groups object. This is the match from a\n * wildcard at the end of a pathname pattern, like `/foo/*`\n */\nconst getTailGroup = (groups: {[key: string]: string | undefined}) => {\n let tailIndex = -1;\n for (const key of Object.keys(groups)) {\n // Anchored. `URLPattern` keys a positional group by its index, so a\n // non-digit key is never one — an unanchored test also accepts a *named*\n // group containing a digit (`:id2`), and since a letter sorts above a\n // digit it then won the comparison below and the param value was handed\n // to the child instead of the tail.\n //\n // Necessary, not sufficient: an unnamed *regex* group is positional too\n // (`/post/(\\d+)` yields key \"0\"), as is a wildcard that is not last\n // (`/foo/*/bar`). Both are mis-read as tails here, and both predate this\n // check — selecting the tail properly needs the pattern, not just groups.\n if (!/^\\d+$/.test(key)) {\n continue;\n }\n // Numeric, not lexicographic: '9' sorts above '10' as a string, so a\n // pattern with eleven or more wildcards picked group 9 as its tail.\n const index = Number(key);\n if (index > tailIndex) {\n tailIndex = index;\n }\n }\n return tailIndex < 0 ? undefined : groups[String(tailIndex)];\n};\n\n/**\n * This event is fired from Routes controllers when their host is connected to\n * announce the child route and potentially connect to a parent routes controller.\n */\nexport class RoutesConnectedEvent extends Event {\n static readonly eventName = 'lit-routes-connected';\n readonly routes: Routes;\n onDisconnect?: () => void;\n\n constructor(routes: Routes) {\n super(RoutesConnectedEvent.eventName, {\n bubbles: true,\n composed: true,\n cancelable: false,\n });\n this.routes = routes;\n }\n}\n\ndeclare global {\n interface HTMLElementEventMap {\n [RoutesConnectedEvent.eventName]: RoutesConnectedEvent;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Modifications Copyright 2026 VanLandingham Labs, same license. See NOTICE.md.\n */\n\n/// <reference types=\"urlpattern-polyfill\" />\n\nimport type {ReactiveController, ReactiveControllerHost} from 'lit';\n\nexport interface BaseRouteConfig {\n name?: string | undefined;\n render?: (params: {[key: string]: string | undefined}) => unknown;\n enter?: (params: {\n [key: string]: string | undefined;\n }) => Promise<boolean> | boolean;\n}\n\n/**\n * A RouteConfig that matches against a `path` string. `path` must be a\n * [`URLPattern` compatible pathname pattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/pathname).\n */\nexport interface PathRouteConfig extends BaseRouteConfig {\n path: string;\n}\n\n/**\n * A RouteConfig that matches against a given [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)\n *\n * While `URLPattern` can match against protocols, hostnames, and ports,\n * routes will only be checked for matches if they're part of the current\n * origin. This means that the pattern is limited to checking `pathname` and\n * `search`.\n */\nexport interface URLPatternRouteConfig extends BaseRouteConfig {\n pattern: URLPatternLike;\n}\n\n/**\n * The part of `URLPattern` this router uses.\n *\n * Declared structurally rather than referencing the global so the emitted\n * `.d.ts` is self-contained: the `/// <reference types=\"urlpattern-polyfill\" />`\n * above is not carried into declaration output, and `URLPattern` is not in\n * TypeScript's bundled `lib.dom`, so a published package typed against the\n * global fails a consumer build with `TS2304: Cannot find name 'URLPattern'`.\n * A real `URLPattern` satisfies this, so passing one still type-checks.\n */\nexport interface URLPatternLike {\n /**\n * The pathname pattern string, as `URLPattern.prototype.pathname` returns\n * it. Read to tell a trailing wildcard from any other positional group —\n * the groups object alone cannot (see `tailOf`).\n */\n readonly pathname: string;\n test(input: {pathname: string}): boolean;\n exec(input: {pathname: string}): {\n pathname: {groups: {[key: string]: string | undefined}};\n } | null;\n}\n\n/**\n * A description of a route, which path or pattern to match against, and a\n * render() callback used to render a match to the outlet.\n */\nexport type RouteConfig = PathRouteConfig | URLPatternRouteConfig;\n\n// A cache of URLPatterns created for PathRouteConfig.\n// Rather than converting all given RoutConfigs to URLPatternRouteConfig, this\n// lets us make `routes` mutable so users can add new PathRouteConfigs\n// dynamically.\nconst patternCache = new WeakMap<PathRouteConfig, URLPatternLike>();\n\nconst isPatternConfig = (route: RouteConfig): route is URLPatternRouteConfig =>\n (route as URLPatternRouteConfig).pattern !== undefined;\n\nconst getPattern = (route: RouteConfig): URLPatternLike => {\n if (isPatternConfig(route)) {\n return route.pattern;\n }\n let pattern = patternCache.get(route);\n if (pattern === undefined) {\n patternCache.set(route, (pattern = new URLPattern({pathname: route.path})));\n }\n return pattern;\n};\n\n/**\n * Matches a pathname pattern that ends in a wildcard, in the forms\n * `URLPattern.prototype.pathname` regenerates one: a bare `*` — which a\n * trailing `(.*)` also normalises to — or `{*}`, which the generator emits for\n * a wildcard after a modified group, e.g. `/docs{/}?*`. Either may be\n * optional (`*?`). Not a wildcard: an escaped `\\*`, or a `*` that is the\n * modifier on a group (`(\\d+)*`, `{/}*`) or on a named param (`:rest*`).\n * Those exclusions matter when an earlier positional group exists —\n * `/x/(\\d+)/:rest*` — since that group would otherwise be taken for the tail.\n */\nconst TRAILING_WILDCARD = /(?:(?<![\\\\)}]|:[\\w$]+)\\*|\\{\\*\\})\\??$/;\n\n/**\n * The tail of a match — what a trailing wildcard (`/foo/*`) captured — or\n * undefined when the pattern has none.\n *\n * Decided from the pattern, not from the groups object: an unnamed regex group\n * (`/post/(\\d+)`) and a wildcard that is not last (`/foo/*` followed by\n * `/bar`) are keyed by index exactly as a tail is, and reading either as one\n * truncated `link()` and handed a child the wrong segment. When a trailing\n * wildcard is present it is the last group in the pattern, so its key is the\n * highest positional index.\n */\nconst tailOf = (\n route: RouteConfig,\n params: {[key: string]: string | undefined}\n): string | undefined => {\n if (!TRAILING_WILDCARD.test(getPattern(route).pathname)) {\n return undefined;\n }\n let tailIndex = -1;\n for (const key of Object.keys(params)) {\n // Numeric, not lexicographic: '9' sorts above '10' as a string, so a\n // pattern with eleven or more wildcards picked group 9 as its tail.\n if (/^\\d+$/.test(key) && Number(key) > tailIndex) {\n tailIndex = Number(key);\n }\n }\n return tailIndex < 0 ? undefined : params[String(tailIndex)];\n};\n\n/**\n * A reactive controller that performs location-based routing using a\n * configuration of URL patterns and associated render callbacks.\n */\nexport class Routes implements ReactiveController {\n private readonly _host: ReactiveControllerHost & HTMLElement;\n\n /*\n * The currently installed set of routes in precedence order.\n *\n * This array is mutable. To dynamically add a new route you can write:\n *\n * ```ts\n * this._routes.routes.push({\n * path: '/foo',\n * render: () => html`<p>Foo</p>`,\n * });\n * ```\n *\n * Mutating this property does not trigger any route transitions. If the\n * changes may result is a different route matching for the current path, you\n * must instigate a route update with `goto()`.\n */\n routes: Array<RouteConfig> = [];\n\n /**\n * A default fallback route which will always be matched if none of the\n * {@link routes} match. Behaves like a `/*` route: `params[0]` is the whole\n * pathname minus its leading slash, and is handed to child controllers as\n * their tail. A nested controller's own pathname is a tail, with no leading\n * slash; the fallback accepts that too.\n */\n fallback?: BaseRouteConfig;\n\n /*\n * The current set of child Routes controllers. These are connected via\n * the routes-connected event.\n */\n private readonly _childRoutes: Array<Routes> = [];\n\n private _parentRoutes: Routes | undefined;\n\n /*\n * State related to the current matching route.\n *\n * We keep this so that consuming code can access current parameters, and so\n * that we can propagate tail matches to child routes if they are added after\n * navigation / matching.\n */\n /** Monotonic goto counter; see the last-goto-wins note in goto(). */\n private _gotoSeq = 0;\n\n private _currentPathname: string | undefined;\n private _currentTail: string | undefined;\n private _currentRoute: RouteConfig | undefined;\n private _currentParams: {\n [key: string]: string | undefined;\n } = {};\n\n /**\n * Callback to call when this controller is disconnected.\n *\n * It's critical to call this immediately in hostDisconnected so that this\n * controller instance doesn't receive a tail match meant for another route.\n */\n // TODO (justinfagnani): Do we need this now that we have a direct reference\n // to the parent? We can call `this._parentRoutes.disconnect(this)`.\n private _onDisconnect: (() => void) | undefined;\n\n constructor(\n host: ReactiveControllerHost & HTMLElement,\n routes: Array<RouteConfig>,\n options?: {fallback?: BaseRouteConfig}\n ) {\n (this._host = host).addController(this);\n this.routes = [...routes];\n this.fallback = options?.fallback;\n }\n\n /**\n * Returns a URL string of the current route, including parent routes,\n * optionally replacing the local path with `pathname`.\n */\n link(pathname?: string): string {\n if (pathname?.startsWith('/')) {\n return pathname;\n }\n if (pathname?.startsWith('.')) {\n throw new Error('Not implemented');\n }\n pathname ??= this._currentPathname;\n return (this._parentRoutes?.link() ?? '') + pathname;\n }\n\n /**\n * Navigates this routes controller to `pathname`.\n *\n * This does not navigate parent routes, so it isn't (yet) a general page\n * navigation API. It does navigate child routes if pathname matches a\n * pattern with a tail wildcard pattern (`/*`).\n *\n * Pass `options.signal` to make the navigation abandonable. `enter()` is\n * awaited, so a second `goto()` can start — and finish — while the first is\n * still resolving its route; without a signal the slower one commits last\n * and the outlet ends up on a route the URL has already left. `Router`\n * threads `NavigateEvent.signal` through for exactly this reason.\n */\n async goto(pathname: string, options?: {signal?: AbortSignal}) {\n // TODO (justinfagnani): handle absolute vs relative paths separately.\n\n // TODO (justinfagnani): generalize this to handle query params and\n // fragments. It currently only handles path names because it's easier to\n // completely disregard the origin for now. The click handler only does\n // an in-page navigation if the origin matches anyway.\n // Last-goto-wins, per controller. The navigation signal alone is not\n // enough: a child controller mounts as a *result* of its parent's render,\n // so its first goto() comes from `_onRoutesConnected` — after the parent's\n // navigation has already finished, and therefore with a signal that will\n // never abort. Without this counter a slow first child load commits over a\n // newer one. This also keeps `Routes` correct when used on its own, with\n // no `Router` and no Navigation API in the picture.\n const seq = ++this._gotoSeq;\n let tail: string | undefined;\n\n if (this.routes.length === 0 && this.fallback === undefined) {\n // If a routes controller has none of its own routes it acts like it has\n // one route of `/*` so that it passes the whole pathname as a tail\n // match.\n tail = pathname;\n this._currentPathname = '';\n // Simulate a tail group with the whole pathname\n this._currentParams = {0: tail};\n } else {\n const match = this._match(pathname);\n if (match === undefined) {\n throw new Error(`No route found for ${pathname}`);\n }\n const {route, params} = match;\n tail = match.tail;\n if (typeof route.enter === 'function') {\n const success = await route.enter(params);\n // If enter() returns false, cancel this navigation\n if (success === false) {\n return;\n }\n }\n // A newer navigation superseded this one while `enter` was awaiting.\n // Committing now would swap the outlet onto a route the URL has left.\n if (options?.signal?.aborted === true || seq !== this._gotoSeq) {\n return;\n }\n // Only update route state if the enter handler completes successfully\n this._currentRoute = route;\n this._currentParams = params;\n this._currentPathname =\n tail === undefined\n ? pathname\n : pathname.substring(0, pathname.length - tail.length);\n }\n this._currentTail = tail;\n\n // Propagate the tail match to children — deliberately NOT awaited.\n //\n // Awaiting looks like it would make `navigation.finished` cover the whole\n // tree, and an earlier revision of this fork did it. It is wrong twice\n // over. At this point `requestUpdate()` has not run, so `_childRoutes`\n // still holds the *outgoing* branch's controller: awaiting it gates the\n // parent's outlet swap on an `enter()` for a tail that controller will\n // never render (a hung one blocks the navigation forever), and if that\n // child has no route for the new tail its `No route found` throw\n // propagates out of here and `requestUpdate()` below never runs — URL\n // committed, outlet stranded, i.e. this fork's own thesis bug one level\n // down. Nested supersession is handled by the goto counter above, not by\n // awaiting. `_routeChild` covers the per-child filtering and error policy.\n //\n // Runs whether or not there is a tail. A route without one has nothing for\n // the children to render, but they must still be superseded — otherwise a\n // child mid-`enter()` for the previous tail stays current and commits over\n // a URL that has moved on.\n for (const childRoutes of this._childRoutes) {\n this._routeChild(childRoutes, tail);\n }\n this._host.requestUpdate();\n }\n\n /**\n * The result of calling the current route's render() callback.\n */\n outlet() {\n return this._currentRoute?.render?.(this._currentParams);\n }\n\n /**\n * The current parsed route parameters.\n */\n get params() {\n return this._currentParams;\n }\n\n /**\n * Hands a tail match to a child controller. Shared by the propagation loop in\n * `goto()` and the late-mount path in `_onRoutesConnected`, so that identical\n * input cannot be silent on one and an uncaught global throw on the other.\n *\n * A child with no route for the new tail is the expected case, not an error —\n * the outgoing branch mid-swap, or a deep link to a path the child cannot\n * render. Filtered structurally rather than by swallowing every rejection, so\n * a genuine `enter()` rejection still surfaces the way it does upstream.\n * Skipping must still supersede: `goto()` is where the counter is bumped, so\n * returning without it would leave an in-flight child navigation current,\n * free to commit over a URL that has moved on. A parent route with no tail\n * at all is the same case: nothing to route, but still something to stand\n * down.\n *\n * No abort signal is threaded through, and the goto is deliberately not\n * awaited. The parent commits its own state before children run, so a child\n * handed an already-aborted signal stands down with no newer goto() arriving\n * to correct it, leaving the nested outlet stuck — reachable, because a\n * hash-only navigation aborts the outstanding one without producing a\n * replacement. Supersession is the counter's job.\n */\n private _routeChild(child: Routes, tail: string | undefined) {\n if (tail === undefined || !child.hasRouteFor(tail)) {\n child._supersede();\n return;\n }\n void child.goto(tail).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n }\n\n /**\n * Invalidate any in-flight `goto()` on this controller without starting a\n * new one. Same-class access, so `_gotoSeq` stays private to `Routes`.\n */\n private _supersede(seen: Set<Routes> = new Set()): void {\n // Unreachable defence in depth. Upstream *can* produce a `_childRoutes`\n // cycle — a host carrying two Routes controllers, disconnected and\n // reconnected, ends up with each registered as the other's child — but\n // `hostDisconnected` below removes the listener that causes it, and a test\n // asserts the cycle cannot form. Kept because an unguarded recursive walk\n // over a cycle is a stack overflow rather than a misrender.\n if (seen.has(this)) {\n return;\n }\n seen.add(this);\n this._gotoSeq++;\n // Recursive: on the navigating branch the child's own propagation loop\n // reaches the grandchildren, but a skipped child never runs one — so\n // without this an in-flight grandchild `enter()` stays current and commits\n // over a URL that has moved on, the same defect one level deeper.\n for (const child of this._childRoutes) {\n child._supersede(seen);\n }\n }\n\n /**\n * True when this controller can render `pathname` — i.e. a route matches, or\n * a fallback is configured.\n *\n * `Router` gates interception on this: intercepting a path we cannot render\n * commits the URL and then throws out of `goto()`, leaving the address bar\n * moved and the outlet stale. Letting the browser handle it instead means a\n * server-rendered page, an export endpoint, or a GET form still works.\n */\n hasRouteFor(pathname: string): boolean {\n // A fallback matches everything, and a controller with no routes of its own\n // behaves as if it had a single `/*` route (goto()'s special case). Either\n // way the answer is yes without running a single pattern — worth\n // short-circuiting, since `Router` asks this on every navigation.\n if (this.fallback !== undefined || this.routes.length === 0) {\n return true;\n }\n // `test()`, not `_match()`: this only needs the yes/no, and `exec()` pays\n // ~8x on a hit to build a groups object the caller would throw away.\n return this.routes.some((r) => getPattern(r).test({pathname}));\n }\n\n /**\n * Matches `pathname` against the installed routes and returns the first match\n * with its parsed parameters, or the fallback's match if one is configured.\n *\n * One `exec()` per candidate rather than `test()` to select and `exec()` to\n * extract: that ran the winning pattern twice, and every caller that wants a\n * route wants its params too.\n */\n private _match(pathname: string):\n | {\n route: RouteConfig;\n params: {[key: string]: string | undefined};\n tail: string | undefined;\n }\n | undefined {\n for (const route of this.routes) {\n const result = getPattern(route).exec({pathname});\n if (result !== null) {\n const params = result.pathname.groups;\n return {route, params, tail: tailOf(route, params)};\n }\n }\n if (this.fallback === undefined) {\n return undefined;\n }\n // The fallback route behaves like it has a \"/*\" path. This is hidden from\n // the public API; the `path` is there to return a valid RouteConfig. The\n // match itself is done by hand rather than with a real `/*` pattern: a\n // nested controller is handed its tail *without* a leading slash, which\n // `/*` does not match, so a nested fallback matched nothing — empty\n // params, no tail, and its own children never routed.\n const tail = pathname.startsWith('/') ? pathname.slice(1) : pathname;\n return {route: {...this.fallback, path: '/*'}, params: {0: tail}, tail};\n }\n\n hostConnected() {\n this._host.addEventListener(\n RoutesConnectedEvent.eventName,\n this._onRoutesConnected\n );\n const event = new RoutesConnectedEvent(this);\n this._host.dispatchEvent(event);\n this._onDisconnect = event.onDisconnect;\n }\n\n hostDisconnected() {\n // Remove the listener hostConnected added. Without this a host that is\n // disconnected and reconnected (a repeat() reorder, a tab swap) leaves the\n // sibling controller's listener installed, so on the second connect it\n // claims the re-dispatching controller as *its* child and the pair point\n // at each other — a real `_childRoutes` cycle, which recursive walks turn\n // into a stack overflow.\n this._host.removeEventListener(\n RoutesConnectedEvent.eventName,\n this._onRoutesConnected\n );\n // When this child routes controller is disconnected because a parent\n // outlet rendered a different template, disconnecting will ensure that\n // this controller doesn't receive a tail match meant for another route.\n this._onDisconnect?.();\n this._parentRoutes = undefined;\n }\n\n private _onRoutesConnected = (e: RoutesConnectedEvent) => {\n // Don't handle the event fired by this routes controller, which we get\n // because we do this.dispatchEvent(...)\n if (e.routes === this) {\n return;\n }\n\n const childRoutes = e.routes;\n this._childRoutes.push(childRoutes);\n childRoutes._parentRoutes = this;\n\n e.stopImmediatePropagation();\n e.onDisconnect = () => {\n const index = this._childRoutes.indexOf(childRoutes);\n if (index !== -1) {\n this._childRoutes.splice(index, 1);\n }\n };\n\n // A child that mounts under an existing tail match has to be caught up to\n // it — it missed the propagation loop in goto() that ran before it existed.\n // With no tail there is nothing to catch up to, and `_routeChild` then only\n // supersedes, a no-op on a freshly mounted child.\n this._routeChild(childRoutes, this._currentTail);\n };\n}\n\n/**\n * This event is fired from Routes controllers when their host is connected to\n * announce the child route and potentially connect to a parent routes controller.\n */\nexport class RoutesConnectedEvent extends Event {\n static readonly eventName = 'lit-routes-connected';\n readonly routes: Routes;\n onDisconnect?: () => void;\n\n constructor(routes: Routes) {\n super(RoutesConnectedEvent.eventName, {\n bubbles: true,\n composed: true,\n cancelable: false,\n });\n this.routes = routes;\n }\n}\n\ndeclare global {\n interface HTMLElementEventMap {\n [RoutesConnectedEvent.eventName]: RoutesConnectedEvent;\n }\n}\n"]}
|
package/package.json
CHANGED
package/src/routes.ts
CHANGED
|
@@ -49,6 +49,12 @@ export interface URLPatternRouteConfig extends BaseRouteConfig {
|
|
|
49
49
|
* A real `URLPattern` satisfies this, so passing one still type-checks.
|
|
50
50
|
*/
|
|
51
51
|
export interface URLPatternLike {
|
|
52
|
+
/**
|
|
53
|
+
* The pathname pattern string, as `URLPattern.prototype.pathname` returns
|
|
54
|
+
* it. Read to tell a trailing wildcard from any other positional group —
|
|
55
|
+
* the groups object alone cannot (see `tailOf`).
|
|
56
|
+
*/
|
|
57
|
+
readonly pathname: string;
|
|
52
58
|
test(input: {pathname: string}): boolean;
|
|
53
59
|
exec(input: {pathname: string}): {
|
|
54
60
|
pathname: {groups: {[key: string]: string | undefined}};
|
|
@@ -81,13 +87,46 @@ const getPattern = (route: RouteConfig): URLPatternLike => {
|
|
|
81
87
|
return pattern;
|
|
82
88
|
};
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Matches a pathname pattern that ends in a wildcard, in the forms
|
|
92
|
+
* `URLPattern.prototype.pathname` regenerates one: a bare `*` — which a
|
|
93
|
+
* trailing `(.*)` also normalises to — or `{*}`, which the generator emits for
|
|
94
|
+
* a wildcard after a modified group, e.g. `/docs{/}?*`. Either may be
|
|
95
|
+
* optional (`*?`). Not a wildcard: an escaped `\*`, or a `*` that is the
|
|
96
|
+
* modifier on a group (`(\d+)*`, `{/}*`) or on a named param (`:rest*`).
|
|
97
|
+
* Those exclusions matter when an earlier positional group exists —
|
|
98
|
+
* `/x/(\d+)/:rest*` — since that group would otherwise be taken for the tail.
|
|
99
|
+
*/
|
|
100
|
+
const TRAILING_WILDCARD = /(?:(?<![\\)}]|:[\w$]+)\*|\{\*\})\??$/;
|
|
88
101
|
|
|
89
|
-
|
|
90
|
-
|
|
102
|
+
/**
|
|
103
|
+
* The tail of a match — what a trailing wildcard (`/foo/*`) captured — or
|
|
104
|
+
* undefined when the pattern has none.
|
|
105
|
+
*
|
|
106
|
+
* Decided from the pattern, not from the groups object: an unnamed regex group
|
|
107
|
+
* (`/post/(\d+)`) and a wildcard that is not last (`/foo/*` followed by
|
|
108
|
+
* `/bar`) are keyed by index exactly as a tail is, and reading either as one
|
|
109
|
+
* truncated `link()` and handed a child the wrong segment. When a trailing
|
|
110
|
+
* wildcard is present it is the last group in the pattern, so its key is the
|
|
111
|
+
* highest positional index.
|
|
112
|
+
*/
|
|
113
|
+
const tailOf = (
|
|
114
|
+
route: RouteConfig,
|
|
115
|
+
params: {[key: string]: string | undefined}
|
|
116
|
+
): string | undefined => {
|
|
117
|
+
if (!TRAILING_WILDCARD.test(getPattern(route).pathname)) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
let tailIndex = -1;
|
|
121
|
+
for (const key of Object.keys(params)) {
|
|
122
|
+
// Numeric, not lexicographic: '9' sorts above '10' as a string, so a
|
|
123
|
+
// pattern with eleven or more wildcards picked group 9 as its tail.
|
|
124
|
+
if (/^\d+$/.test(key) && Number(key) > tailIndex) {
|
|
125
|
+
tailIndex = Number(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return tailIndex < 0 ? undefined : params[String(tailIndex)];
|
|
129
|
+
};
|
|
91
130
|
|
|
92
131
|
/**
|
|
93
132
|
* A reactive controller that performs location-based routing using a
|
|
@@ -116,7 +155,10 @@ export class Routes implements ReactiveController {
|
|
|
116
155
|
|
|
117
156
|
/**
|
|
118
157
|
* A default fallback route which will always be matched if none of the
|
|
119
|
-
* {@link routes} match.
|
|
158
|
+
* {@link routes} match. Behaves like a `/*` route: `params[0]` is the whole
|
|
159
|
+
* pathname minus its leading slash, and is handed to child controllers as
|
|
160
|
+
* their tail. A nested controller's own pathname is a tail, with no leading
|
|
161
|
+
* slash; the fallback accepts that too.
|
|
120
162
|
*/
|
|
121
163
|
fallback?: BaseRouteConfig;
|
|
122
164
|
|
|
@@ -139,6 +181,7 @@ export class Routes implements ReactiveController {
|
|
|
139
181
|
private _gotoSeq = 0;
|
|
140
182
|
|
|
141
183
|
private _currentPathname: string | undefined;
|
|
184
|
+
private _currentTail: string | undefined;
|
|
142
185
|
private _currentRoute: RouteConfig | undefined;
|
|
143
186
|
private _currentParams: {
|
|
144
187
|
[key: string]: string | undefined;
|
|
@@ -207,23 +250,23 @@ export class Routes implements ReactiveController {
|
|
|
207
250
|
// newer one. This also keeps `Routes` correct when used on its own, with
|
|
208
251
|
// no `Router` and no Navigation API in the picture.
|
|
209
252
|
const seq = ++this._gotoSeq;
|
|
210
|
-
let
|
|
253
|
+
let tail: string | undefined;
|
|
211
254
|
|
|
212
255
|
if (this.routes.length === 0 && this.fallback === undefined) {
|
|
213
256
|
// If a routes controller has none of its own routes it acts like it has
|
|
214
257
|
// one route of `/*` so that it passes the whole pathname as a tail
|
|
215
258
|
// match.
|
|
216
|
-
|
|
259
|
+
tail = pathname;
|
|
217
260
|
this._currentPathname = '';
|
|
218
261
|
// Simulate a tail group with the whole pathname
|
|
219
|
-
this._currentParams = {0:
|
|
262
|
+
this._currentParams = {0: tail};
|
|
220
263
|
} else {
|
|
221
264
|
const match = this._match(pathname);
|
|
222
265
|
if (match === undefined) {
|
|
223
266
|
throw new Error(`No route found for ${pathname}`);
|
|
224
267
|
}
|
|
225
268
|
const {route, params} = match;
|
|
226
|
-
|
|
269
|
+
tail = match.tail;
|
|
227
270
|
if (typeof route.enter === 'function') {
|
|
228
271
|
const success = await route.enter(params);
|
|
229
272
|
// If enter() returns false, cancel this navigation
|
|
@@ -240,10 +283,11 @@ export class Routes implements ReactiveController {
|
|
|
240
283
|
this._currentRoute = route;
|
|
241
284
|
this._currentParams = params;
|
|
242
285
|
this._currentPathname =
|
|
243
|
-
|
|
286
|
+
tail === undefined
|
|
244
287
|
? pathname
|
|
245
|
-
: pathname.substring(0, pathname.length -
|
|
288
|
+
: pathname.substring(0, pathname.length - tail.length);
|
|
246
289
|
}
|
|
290
|
+
this._currentTail = tail;
|
|
247
291
|
|
|
248
292
|
// Propagate the tail match to children — deliberately NOT awaited.
|
|
249
293
|
//
|
|
@@ -258,10 +302,13 @@ export class Routes implements ReactiveController {
|
|
|
258
302
|
// committed, outlet stranded, i.e. this fork's own thesis bug one level
|
|
259
303
|
// down. Nested supersession is handled by the goto counter above, not by
|
|
260
304
|
// awaiting. `_routeChild` covers the per-child filtering and error policy.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
305
|
+
//
|
|
306
|
+
// Runs whether or not there is a tail. A route without one has nothing for
|
|
307
|
+
// the children to render, but they must still be superseded — otherwise a
|
|
308
|
+
// child mid-`enter()` for the previous tail stays current and commits over
|
|
309
|
+
// a URL that has moved on.
|
|
310
|
+
for (const childRoutes of this._childRoutes) {
|
|
311
|
+
this._routeChild(childRoutes, tail);
|
|
265
312
|
}
|
|
266
313
|
this._host.requestUpdate();
|
|
267
314
|
}
|
|
@@ -291,7 +338,9 @@ export class Routes implements ReactiveController {
|
|
|
291
338
|
* a genuine `enter()` rejection still surfaces the way it does upstream.
|
|
292
339
|
* Skipping must still supersede: `goto()` is where the counter is bumped, so
|
|
293
340
|
* returning without it would leave an in-flight child navigation current,
|
|
294
|
-
* free to commit over a URL that has moved on.
|
|
341
|
+
* free to commit over a URL that has moved on. A parent route with no tail
|
|
342
|
+
* at all is the same case: nothing to route, but still something to stand
|
|
343
|
+
* down.
|
|
295
344
|
*
|
|
296
345
|
* No abort signal is threaded through, and the goto is deliberately not
|
|
297
346
|
* awaited. The parent commits its own state before children run, so a child
|
|
@@ -300,8 +349,8 @@ export class Routes implements ReactiveController {
|
|
|
300
349
|
* hash-only navigation aborts the outstanding one without producing a
|
|
301
350
|
* replacement. Supersession is the counter's job.
|
|
302
351
|
*/
|
|
303
|
-
private _routeChild(child: Routes, tail: string) {
|
|
304
|
-
if (!child.hasRouteFor(tail)) {
|
|
352
|
+
private _routeChild(child: Routes, tail: string | undefined) {
|
|
353
|
+
if (tail === undefined || !child.hasRouteFor(tail)) {
|
|
305
354
|
child._supersede();
|
|
306
355
|
return;
|
|
307
356
|
}
|
|
@@ -368,27 +417,30 @@ export class Routes implements ReactiveController {
|
|
|
368
417
|
* route wants its params too.
|
|
369
418
|
*/
|
|
370
419
|
private _match(pathname: string):
|
|
371
|
-
| {
|
|
420
|
+
| {
|
|
421
|
+
route: RouteConfig;
|
|
422
|
+
params: {[key: string]: string | undefined};
|
|
423
|
+
tail: string | undefined;
|
|
424
|
+
}
|
|
372
425
|
| undefined {
|
|
373
426
|
for (const route of this.routes) {
|
|
374
427
|
const result = getPattern(route).exec({pathname});
|
|
375
428
|
if (result !== null) {
|
|
376
|
-
|
|
429
|
+
const params = result.pathname.groups;
|
|
430
|
+
return {route, params, tail: tailOf(route, params)};
|
|
377
431
|
}
|
|
378
432
|
}
|
|
379
433
|
if (this.fallback === undefined) {
|
|
380
434
|
return undefined;
|
|
381
435
|
}
|
|
382
436
|
// The fallback route behaves like it has a "/*" path. This is hidden from
|
|
383
|
-
// the public API
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
params: wildcard.exec({pathname})?.pathname.groups ?? {},
|
|
391
|
-
};
|
|
437
|
+
// the public API; the `path` is there to return a valid RouteConfig. The
|
|
438
|
+
// match itself is done by hand rather than with a real `/*` pattern: a
|
|
439
|
+
// nested controller is handed its tail *without* a leading slash, which
|
|
440
|
+
// `/*` does not match, so a nested fallback matched nothing — empty
|
|
441
|
+
// params, no tail, and its own children never routed.
|
|
442
|
+
const tail = pathname.startsWith('/') ? pathname.slice(1) : pathname;
|
|
443
|
+
return {route: {...this.fallback, path: '/*'}, params: {0: tail}, tail};
|
|
392
444
|
}
|
|
393
445
|
|
|
394
446
|
hostConnected() {
|
|
@@ -440,43 +492,12 @@ export class Routes implements ReactiveController {
|
|
|
440
492
|
|
|
441
493
|
// A child that mounts under an existing tail match has to be caught up to
|
|
442
494
|
// it — it missed the propagation loop in goto() that ran before it existed.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
495
|
+
// With no tail there is nothing to catch up to, and `_routeChild` then only
|
|
496
|
+
// supersedes, a no-op on a freshly mounted child.
|
|
497
|
+
this._routeChild(childRoutes, this._currentTail);
|
|
447
498
|
};
|
|
448
499
|
}
|
|
449
500
|
|
|
450
|
-
/**
|
|
451
|
-
* Returns the tail of a pathname groups object. This is the match from a
|
|
452
|
-
* wildcard at the end of a pathname pattern, like `/foo/*`
|
|
453
|
-
*/
|
|
454
|
-
const getTailGroup = (groups: {[key: string]: string | undefined}) => {
|
|
455
|
-
let tailIndex = -1;
|
|
456
|
-
for (const key of Object.keys(groups)) {
|
|
457
|
-
// Anchored. `URLPattern` keys a positional group by its index, so a
|
|
458
|
-
// non-digit key is never one — an unanchored test also accepts a *named*
|
|
459
|
-
// group containing a digit (`:id2`), and since a letter sorts above a
|
|
460
|
-
// digit it then won the comparison below and the param value was handed
|
|
461
|
-
// to the child instead of the tail.
|
|
462
|
-
//
|
|
463
|
-
// Necessary, not sufficient: an unnamed *regex* group is positional too
|
|
464
|
-
// (`/post/(\d+)` yields key "0"), as is a wildcard that is not last
|
|
465
|
-
// (`/foo/*/bar`). Both are mis-read as tails here, and both predate this
|
|
466
|
-
// check — selecting the tail properly needs the pattern, not just groups.
|
|
467
|
-
if (!/^\d+$/.test(key)) {
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
// Numeric, not lexicographic: '9' sorts above '10' as a string, so a
|
|
471
|
-
// pattern with eleven or more wildcards picked group 9 as its tail.
|
|
472
|
-
const index = Number(key);
|
|
473
|
-
if (index > tailIndex) {
|
|
474
|
-
tailIndex = index;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return tailIndex < 0 ? undefined : groups[String(tailIndex)];
|
|
478
|
-
};
|
|
479
|
-
|
|
480
501
|
/**
|
|
481
502
|
* This event is fired from Routes controllers when their host is connected to
|
|
482
503
|
* announce the child route and potentially connect to a parent routes controller.
|