lit-navigation-router 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/NOTICE.md +21 -5
- package/README.md +56 -0
- package/development/router.d.ts.map +1 -1
- package/development/router.js +21 -8
- package/development/router.js.map +1 -1
- package/development/routes.d.ts +75 -13
- package/development/routes.d.ts.map +1 -1
- package/development/routes.js +158 -74
- package/development/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/router.ts +24 -10
- package/src/routes.ts +211 -82
package/src/routes.ts
CHANGED
|
@@ -31,8 +31,8 @@ export interface PathRouteConfig extends BaseRouteConfig {
|
|
|
31
31
|
*
|
|
32
32
|
* While `URLPattern` can match against protocols, hostnames, and ports,
|
|
33
33
|
* routes will only be checked for matches if they're part of the current
|
|
34
|
-
* origin. This means
|
|
35
|
-
* `
|
|
34
|
+
* origin. This means the pattern is limited to checking `pathname`, `search`
|
|
35
|
+
* and `hash`.
|
|
36
36
|
*/
|
|
37
37
|
export interface URLPatternRouteConfig extends BaseRouteConfig {
|
|
38
38
|
pattern: URLPatternLike;
|
|
@@ -49,12 +49,68 @@ 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
|
-
|
|
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;
|
|
58
|
+
/**
|
|
59
|
+
* The hash pattern string. `'*'` means the route places no constraint on the
|
|
60
|
+
* fragment, which is what `URLPattern` fills in for any component the caller
|
|
61
|
+
* omits. `Router` reads this to decide whether a fragment-only navigation is
|
|
62
|
+
* its business or the browser's.
|
|
63
|
+
*/
|
|
64
|
+
readonly hash: string;
|
|
65
|
+
test(input: RouteLocation): boolean;
|
|
66
|
+
exec(input: RouteLocation): {
|
|
54
67
|
pathname: {groups: {[key: string]: string | undefined}};
|
|
68
|
+
search: {groups: {[key: string]: string | undefined}};
|
|
69
|
+
hash: {groups: {[key: string]: string | undefined}};
|
|
55
70
|
} | null;
|
|
56
71
|
}
|
|
57
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The parts of a location this router matches against, split out of a path
|
|
75
|
+
* string by `parseLocation`.
|
|
76
|
+
*
|
|
77
|
+
* `search` and `hash` carry no leading `?` or `#`. `URLPattern` canonicalises
|
|
78
|
+
* either form away on an init input, but only for a real `URLPattern` — the
|
|
79
|
+
* structural `URLPatternLike` above admits other implementations, so the
|
|
80
|
+
* delimiters are stripped here rather than left to the pattern.
|
|
81
|
+
*/
|
|
82
|
+
export interface RouteLocation {
|
|
83
|
+
pathname: string;
|
|
84
|
+
search: string;
|
|
85
|
+
hash: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Splits `path` into the components a pattern is matched against.
|
|
90
|
+
*
|
|
91
|
+
* Deliberately not `new URL(path, origin)`: a nested controller's path is a
|
|
92
|
+
* *tail* — a bare relative segment like `abc` — which `URL` would resolve
|
|
93
|
+
* against the current directory and mangle.
|
|
94
|
+
*/
|
|
95
|
+
const parseLocation = (path: string): RouteLocation => {
|
|
96
|
+
const hashIndex = path.indexOf('#');
|
|
97
|
+
const hash = hashIndex === -1 ? '' : path.slice(hashIndex + 1);
|
|
98
|
+
const beforeHash = hashIndex === -1 ? path : path.slice(0, hashIndex);
|
|
99
|
+
const searchIndex = beforeHash.indexOf('?');
|
|
100
|
+
return {
|
|
101
|
+
pathname: searchIndex === -1 ? beforeHash : beforeHash.slice(0, searchIndex),
|
|
102
|
+
search: searchIndex === -1 ? '' : beforeHash.slice(searchIndex + 1),
|
|
103
|
+
hash,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** Re-attaches `search` and `hash` to a pathname. Inverse of `parseLocation`. */
|
|
108
|
+
const formatLocation = (
|
|
109
|
+
pathname: string,
|
|
110
|
+
{search, hash}: {search: string; hash: string}
|
|
111
|
+
): string =>
|
|
112
|
+
pathname + (search === '' ? '' : `?${search}`) + (hash === '' ? '' : `#${hash}`);
|
|
113
|
+
|
|
58
114
|
/**
|
|
59
115
|
* A description of a route, which path or pattern to match against, and a
|
|
60
116
|
* render() callback used to render a match to the outlet.
|
|
@@ -81,13 +137,46 @@ const getPattern = (route: RouteConfig): URLPatternLike => {
|
|
|
81
137
|
return pattern;
|
|
82
138
|
};
|
|
83
139
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Matches a pathname pattern that ends in a wildcard, in the forms
|
|
142
|
+
* `URLPattern.prototype.pathname` regenerates one: a bare `*` — which a
|
|
143
|
+
* trailing `(.*)` also normalises to — or `{*}`, which the generator emits for
|
|
144
|
+
* a wildcard after a modified group, e.g. `/docs{/}?*`. Either may be
|
|
145
|
+
* optional (`*?`). Not a wildcard: an escaped `\*`, or a `*` that is the
|
|
146
|
+
* modifier on a group (`(\d+)*`, `{/}*`) or on a named param (`:rest*`).
|
|
147
|
+
* Those exclusions matter when an earlier positional group exists —
|
|
148
|
+
* `/x/(\d+)/:rest*` — since that group would otherwise be taken for the tail.
|
|
149
|
+
*/
|
|
150
|
+
const TRAILING_WILDCARD = /(?:(?<![\\)}]|:[\w$]+)\*|\{\*\})\??$/;
|
|
88
151
|
|
|
89
|
-
|
|
90
|
-
|
|
152
|
+
/**
|
|
153
|
+
* The tail of a match — what a trailing wildcard (`/foo/*`) captured — or
|
|
154
|
+
* undefined when the pattern has none.
|
|
155
|
+
*
|
|
156
|
+
* Decided from the pattern, not from the groups object: an unnamed regex group
|
|
157
|
+
* (`/post/(\d+)`) and a wildcard that is not last (`/foo/*` followed by
|
|
158
|
+
* `/bar`) are keyed by index exactly as a tail is, and reading either as one
|
|
159
|
+
* truncated `link()` and handed a child the wrong segment. When a trailing
|
|
160
|
+
* wildcard is present it is the last group in the pattern, so its key is the
|
|
161
|
+
* highest positional index.
|
|
162
|
+
*/
|
|
163
|
+
const tailOf = (
|
|
164
|
+
route: RouteConfig,
|
|
165
|
+
params: {[key: string]: string | undefined}
|
|
166
|
+
): string | undefined => {
|
|
167
|
+
if (!TRAILING_WILDCARD.test(getPattern(route).pathname)) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
let tailIndex = -1;
|
|
171
|
+
for (const key of Object.keys(params)) {
|
|
172
|
+
// Numeric, not lexicographic: '9' sorts above '10' as a string, so a
|
|
173
|
+
// pattern with eleven or more wildcards picked group 9 as its tail.
|
|
174
|
+
if (/^\d+$/.test(key) && Number(key) > tailIndex) {
|
|
175
|
+
tailIndex = Number(key);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return tailIndex < 0 ? undefined : params[String(tailIndex)];
|
|
179
|
+
};
|
|
91
180
|
|
|
92
181
|
/**
|
|
93
182
|
* A reactive controller that performs location-based routing using a
|
|
@@ -116,7 +205,10 @@ export class Routes implements ReactiveController {
|
|
|
116
205
|
|
|
117
206
|
/**
|
|
118
207
|
* A default fallback route which will always be matched if none of the
|
|
119
|
-
* {@link routes} match.
|
|
208
|
+
* {@link routes} match. Behaves like a `/*` route: `params[0]` is the whole
|
|
209
|
+
* pathname minus its leading slash, and is handed to child controllers as
|
|
210
|
+
* their tail. A nested controller's own pathname is a tail, with no leading
|
|
211
|
+
* slash; the fallback accepts that too.
|
|
120
212
|
*/
|
|
121
213
|
fallback?: BaseRouteConfig;
|
|
122
214
|
|
|
@@ -139,6 +231,16 @@ export class Routes implements ReactiveController {
|
|
|
139
231
|
private _gotoSeq = 0;
|
|
140
232
|
|
|
141
233
|
private _currentPathname: string | undefined;
|
|
234
|
+
private _currentTail: string | undefined;
|
|
235
|
+
/**
|
|
236
|
+
* The search and hash of the current location.
|
|
237
|
+
*
|
|
238
|
+
* Ambient rather than tailed: only the pathname nests, so a child controller
|
|
239
|
+
* is handed the parent's tail as its pathname but the *same* search and
|
|
240
|
+
* hash. There is no meaningful way to split a fragment across a route tree.
|
|
241
|
+
*/
|
|
242
|
+
private _currentSearch = '';
|
|
243
|
+
private _currentHash = '';
|
|
142
244
|
private _currentRoute: RouteConfig | undefined;
|
|
143
245
|
private _currentParams: {
|
|
144
246
|
[key: string]: string | undefined;
|
|
@@ -192,13 +294,12 @@ export class Routes implements ReactiveController {
|
|
|
192
294
|
* and the outlet ends up on a route the URL has already left. `Router`
|
|
193
295
|
* threads `NavigateEvent.signal` through for exactly this reason.
|
|
194
296
|
*/
|
|
195
|
-
async goto(
|
|
297
|
+
async goto(path: string, options?: {signal?: AbortSignal}) {
|
|
196
298
|
// TODO (justinfagnani): handle absolute vs relative paths separately.
|
|
197
299
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// an in-page navigation if the origin matches anyway.
|
|
300
|
+
const location = parseLocation(path);
|
|
301
|
+
const {pathname} = location;
|
|
302
|
+
|
|
202
303
|
// Last-goto-wins, per controller. The navigation signal alone is not
|
|
203
304
|
// enough: a child controller mounts as a *result* of its parent's render,
|
|
204
305
|
// so its first goto() comes from `_onRoutesConnected` — after the parent's
|
|
@@ -207,23 +308,23 @@ export class Routes implements ReactiveController {
|
|
|
207
308
|
// newer one. This also keeps `Routes` correct when used on its own, with
|
|
208
309
|
// no `Router` and no Navigation API in the picture.
|
|
209
310
|
const seq = ++this._gotoSeq;
|
|
210
|
-
let
|
|
311
|
+
let tail: string | undefined;
|
|
211
312
|
|
|
212
313
|
if (this.routes.length === 0 && this.fallback === undefined) {
|
|
213
314
|
// If a routes controller has none of its own routes it acts like it has
|
|
214
315
|
// one route of `/*` so that it passes the whole pathname as a tail
|
|
215
316
|
// match.
|
|
216
|
-
|
|
317
|
+
tail = pathname;
|
|
217
318
|
this._currentPathname = '';
|
|
218
319
|
// Simulate a tail group with the whole pathname
|
|
219
|
-
this._currentParams = {0:
|
|
320
|
+
this._currentParams = {0: tail};
|
|
220
321
|
} else {
|
|
221
|
-
const match = this._match(
|
|
322
|
+
const match = this._match(location);
|
|
222
323
|
if (match === undefined) {
|
|
223
|
-
throw new Error(`No route found for ${
|
|
324
|
+
throw new Error(`No route found for ${path}`);
|
|
224
325
|
}
|
|
225
326
|
const {route, params} = match;
|
|
226
|
-
|
|
327
|
+
tail = match.tail;
|
|
227
328
|
if (typeof route.enter === 'function') {
|
|
228
329
|
const success = await route.enter(params);
|
|
229
330
|
// If enter() returns false, cancel this navigation
|
|
@@ -240,10 +341,13 @@ export class Routes implements ReactiveController {
|
|
|
240
341
|
this._currentRoute = route;
|
|
241
342
|
this._currentParams = params;
|
|
242
343
|
this._currentPathname =
|
|
243
|
-
|
|
344
|
+
tail === undefined
|
|
244
345
|
? pathname
|
|
245
|
-
: pathname.substring(0, pathname.length -
|
|
346
|
+
: pathname.substring(0, pathname.length - tail.length);
|
|
246
347
|
}
|
|
348
|
+
this._currentTail = tail;
|
|
349
|
+
this._currentSearch = location.search;
|
|
350
|
+
this._currentHash = location.hash;
|
|
247
351
|
|
|
248
352
|
// Propagate the tail match to children — deliberately NOT awaited.
|
|
249
353
|
//
|
|
@@ -258,10 +362,13 @@ export class Routes implements ReactiveController {
|
|
|
258
362
|
// committed, outlet stranded, i.e. this fork's own thesis bug one level
|
|
259
363
|
// down. Nested supersession is handled by the goto counter above, not by
|
|
260
364
|
// awaiting. `_routeChild` covers the per-child filtering and error policy.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
365
|
+
//
|
|
366
|
+
// Runs whether or not there is a tail. A route without one has nothing for
|
|
367
|
+
// the children to render, but they must still be superseded — otherwise a
|
|
368
|
+
// child mid-`enter()` for the previous tail stays current and commits over
|
|
369
|
+
// a URL that has moved on.
|
|
370
|
+
for (const childRoutes of this._childRoutes) {
|
|
371
|
+
this._routeChild(childRoutes, tail);
|
|
265
372
|
}
|
|
266
373
|
this._host.requestUpdate();
|
|
267
374
|
}
|
|
@@ -291,7 +398,9 @@ export class Routes implements ReactiveController {
|
|
|
291
398
|
* a genuine `enter()` rejection still surfaces the way it does upstream.
|
|
292
399
|
* Skipping must still supersede: `goto()` is where the counter is bumped, so
|
|
293
400
|
* returning without it would leave an in-flight child navigation current,
|
|
294
|
-
* free to commit over a URL that has moved on.
|
|
401
|
+
* free to commit over a URL that has moved on. A parent route with no tail
|
|
402
|
+
* at all is the same case: nothing to route, but still something to stand
|
|
403
|
+
* down.
|
|
295
404
|
*
|
|
296
405
|
* No abort signal is threaded through, and the goto is deliberately not
|
|
297
406
|
* awaited. The parent commits its own state before children run, so a child
|
|
@@ -300,12 +409,20 @@ export class Routes implements ReactiveController {
|
|
|
300
409
|
* hash-only navigation aborts the outstanding one without producing a
|
|
301
410
|
* replacement. Supersession is the counter's job.
|
|
302
411
|
*/
|
|
303
|
-
private _routeChild(child: Routes, tail: string) {
|
|
304
|
-
if (
|
|
412
|
+
private _routeChild(child: Routes, tail: string | undefined) {
|
|
413
|
+
if (tail === undefined) {
|
|
414
|
+
child._supersede();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const childPath = formatLocation(tail, {
|
|
418
|
+
search: this._currentSearch,
|
|
419
|
+
hash: this._currentHash,
|
|
420
|
+
});
|
|
421
|
+
if (!child.hasRouteFor(childPath)) {
|
|
305
422
|
child._supersede();
|
|
306
423
|
return;
|
|
307
424
|
}
|
|
308
|
-
void child.goto(
|
|
425
|
+
void child.goto(childPath).catch((err) => {
|
|
309
426
|
queueMicrotask(() => {
|
|
310
427
|
throw err;
|
|
311
428
|
});
|
|
@@ -338,7 +455,32 @@ export class Routes implements ReactiveController {
|
|
|
338
455
|
}
|
|
339
456
|
|
|
340
457
|
/**
|
|
341
|
-
* True when this controller
|
|
458
|
+
* True when this controller, or any controller below it, has a route that
|
|
459
|
+
* constrains the hash.
|
|
460
|
+
*
|
|
461
|
+
* `Router` gates interception of fragment-only navigation on this. Left
|
|
462
|
+
* ungated, a pathname-only app would have every in-page anchor swallowed and
|
|
463
|
+
* re-rendered instead of scrolled; gated, such an app behaves exactly as it
|
|
464
|
+
* did before hash routes existed.
|
|
465
|
+
*
|
|
466
|
+
* Walks children because a nested controller may route on the hash while the
|
|
467
|
+
* top-level `Router` does not — and only the top-level one sees the
|
|
468
|
+
* navigate event.
|
|
469
|
+
*/
|
|
470
|
+
protected _constrainsHash(seen: Set<Routes> = new Set()): boolean {
|
|
471
|
+
// Cycle guard, matching `_supersede`; see the note there.
|
|
472
|
+
if (seen.has(this)) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
seen.add(this);
|
|
476
|
+
return (
|
|
477
|
+
this.routes.some((r) => getPattern(r).hash !== '*') ||
|
|
478
|
+
this._childRoutes.some((c) => c._constrainsHash(seen))
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* True when this controller can render `path` — i.e. a route matches, or
|
|
342
484
|
* a fallback is configured.
|
|
343
485
|
*
|
|
344
486
|
* `Router` gates interception on this: intercepting a path we cannot render
|
|
@@ -346,7 +488,7 @@ export class Routes implements ReactiveController {
|
|
|
346
488
|
* moved and the outlet stale. Letting the browser handle it instead means a
|
|
347
489
|
* server-rendered page, an export endpoint, or a GET form still works.
|
|
348
490
|
*/
|
|
349
|
-
hasRouteFor(
|
|
491
|
+
hasRouteFor(path: string): boolean {
|
|
350
492
|
// A fallback matches everything, and a controller with no routes of its own
|
|
351
493
|
// behaves as if it had a single `/*` route (goto()'s special case). Either
|
|
352
494
|
// way the answer is yes without running a single pattern — worth
|
|
@@ -356,7 +498,8 @@ export class Routes implements ReactiveController {
|
|
|
356
498
|
}
|
|
357
499
|
// `test()`, not `_match()`: this only needs the yes/no, and `exec()` pays
|
|
358
500
|
// ~8x on a hit to build a groups object the caller would throw away.
|
|
359
|
-
|
|
501
|
+
const location = parseLocation(path);
|
|
502
|
+
return this.routes.some((r) => getPattern(r).test(location));
|
|
360
503
|
}
|
|
361
504
|
|
|
362
505
|
/**
|
|
@@ -367,28 +510,45 @@ export class Routes implements ReactiveController {
|
|
|
367
510
|
* extract: that ran the winning pattern twice, and every caller that wants a
|
|
368
511
|
* route wants its params too.
|
|
369
512
|
*/
|
|
370
|
-
private _match(
|
|
371
|
-
| {
|
|
513
|
+
private _match(location: RouteLocation):
|
|
514
|
+
| {
|
|
515
|
+
route: RouteConfig;
|
|
516
|
+
params: {[key: string]: string | undefined};
|
|
517
|
+
tail: string | undefined;
|
|
518
|
+
}
|
|
372
519
|
| undefined {
|
|
373
520
|
for (const route of this.routes) {
|
|
374
|
-
const result = getPattern(route).exec(
|
|
521
|
+
const result = getPattern(route).exec(location);
|
|
375
522
|
if (result !== null) {
|
|
376
|
-
|
|
523
|
+
const params: {[key: string]: string | undefined} = {
|
|
524
|
+
...result.pathname.groups,
|
|
525
|
+
};
|
|
526
|
+
// Named groups only. A positional group is keyed by index in every
|
|
527
|
+
// component independently, so `/child/*` with a hash of `*` yields a
|
|
528
|
+
// "0" in both — and `tailOf` picks the tail by highest numeric key.
|
|
529
|
+
// Merging them would let a fragment masquerade as the tail.
|
|
530
|
+
for (const groups of [result.search.groups, result.hash.groups]) {
|
|
531
|
+
for (const [key, value] of Object.entries(groups)) {
|
|
532
|
+
if (!/^\d+$/.test(key)) {
|
|
533
|
+
params[key] = value;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return {route, params, tail: tailOf(route, result.pathname.groups)};
|
|
377
538
|
}
|
|
378
539
|
}
|
|
379
540
|
if (this.fallback === undefined) {
|
|
380
541
|
return undefined;
|
|
381
542
|
}
|
|
382
543
|
// The fallback route behaves like it has a "/*" path. This is hidden from
|
|
383
|
-
// the public API
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
};
|
|
544
|
+
// the public API; the `path` is there to return a valid RouteConfig. The
|
|
545
|
+
// match itself is done by hand rather than with a real `/*` pattern: a
|
|
546
|
+
// nested controller is handed its tail *without* a leading slash, which
|
|
547
|
+
// `/*` does not match, so a nested fallback matched nothing — empty
|
|
548
|
+
// params, no tail, and its own children never routed.
|
|
549
|
+
const {pathname} = location;
|
|
550
|
+
const tail = pathname.startsWith('/') ? pathname.slice(1) : pathname;
|
|
551
|
+
return {route: {...this.fallback, path: '/*'}, params: {0: tail}, tail};
|
|
392
552
|
}
|
|
393
553
|
|
|
394
554
|
hostConnected() {
|
|
@@ -440,43 +600,12 @@ export class Routes implements ReactiveController {
|
|
|
440
600
|
|
|
441
601
|
// A child that mounts under an existing tail match has to be caught up to
|
|
442
602
|
// it — it missed the propagation loop in goto() that ran before it existed.
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
603
|
+
// With no tail there is nothing to catch up to, and `_routeChild` then only
|
|
604
|
+
// supersedes, a no-op on a freshly mounted child.
|
|
605
|
+
this._routeChild(childRoutes, this._currentTail);
|
|
447
606
|
};
|
|
448
607
|
}
|
|
449
608
|
|
|
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
609
|
/**
|
|
481
610
|
* This event is fired from Routes controllers when their host is connected to
|
|
482
611
|
* announce the child route and potentially connect to a parent routes controller.
|