lit-navigation-router 0.2.0 → 0.3.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 +53 -0
- package/development/router.d.ts.map +1 -1
- package/development/router.js +13 -7
- package/development/router.js.map +1 -1
- package/development/routes.d.ts +28 -2
- package/development/routes.d.ts.map +1 -1
- package/development/routes.js +102 -71
- package/development/routes.js.map +1 -1
- package/package.json +3 -2
- package/src/router.ts +15 -8
- package/src/routes.ts +108 -76
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The wildcard tail handed to child controllers was selected incorrectly.**
|
|
8
|
+
`getTailGroup()` picked the winning positional group with an unanchored
|
|
9
|
+
`/\d+/` test and a string comparison, which went wrong two ways:
|
|
10
|
+
|
|
11
|
+
- A *named* group whose name merely contains a digit was accepted as a
|
|
12
|
+
candidate, and since a letter sorts above a digit it then won. A route like
|
|
13
|
+
`/user/:id2/*` on `/user/5/docs/a` handed the child `'5'` instead of
|
|
14
|
+
`'docs/a'`, and the child rendered nothing.
|
|
15
|
+
- `'9' > '10'` as strings, so a pattern with eleven or more wildcards took
|
|
16
|
+
the second-to-last group as its tail.
|
|
17
|
+
|
|
18
|
+
**This changes behaviour.** If a route combines a wildcard with a param name
|
|
19
|
+
containing a digit, the child controller now receives a different path — the
|
|
20
|
+
correct one. Anything relying on the old selection was relying on the child
|
|
21
|
+
being given the wrong segment.
|
|
22
|
+
|
|
23
|
+
- **An unset `formData` or `downloadRequest` on a `NavigateEvent` no longer
|
|
24
|
+
makes the router decline every navigation.** Both are spec'd as
|
|
25
|
+
nullable-but-present, so the previous strict `!== null` was correct against a
|
|
26
|
+
real Navigation API but wrong under a polyfill that leaves either unset.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Merged the two child-routing paths (`goto()`'s propagation loop and the
|
|
31
|
+
late-mount path in `_onRoutesConnected`) into a single `_routeChild()`, so
|
|
32
|
+
they cannot diverge. A child skipped on the late-mount path is now also
|
|
33
|
+
superseded.
|
|
34
|
+
- `hasRouteFor()` short-circuits when a fallback is configured instead of
|
|
35
|
+
running every pattern, and the fallback no longer rebuilds a `URLPattern` on
|
|
36
|
+
every navigation.
|
|
37
|
+
- `location.origin` is read per navigation rather than at module scope, so
|
|
38
|
+
importing the package no longer touches `location` — importing it where there
|
|
39
|
+
is no DOM previously threw, contradicting `sideEffects: false`.
|
|
40
|
+
|
|
41
|
+
### Known issues
|
|
42
|
+
|
|
43
|
+
`getTailGroup()` still cannot distinguish the trailing wildcard from an unnamed
|
|
44
|
+
regex group or a wildcard that is not last. See
|
|
45
|
+
[#4](https://github.com/VanLandinghamLabs/lit-router/issues/4),
|
|
46
|
+
[#5](https://github.com/VanLandinghamLabs/lit-router/issues/5),
|
|
47
|
+
[#6](https://github.com/VanLandinghamLabs/lit-router/issues/6), and
|
|
48
|
+
[#7](https://github.com/VanLandinghamLabs/lit-router/issues/7).
|
|
49
|
+
|
|
50
|
+
## 0.2.0
|
|
51
|
+
|
|
52
|
+
Initial release of the fork. A router for Lit built on the Navigation API,
|
|
53
|
+
forked from `@lit-labs/router`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAqBnC,uEAAuE;AACvE,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAC;IAC3C,MAAM,CAAC,EAAE,kBAAkB,GAAG,QAAQ,CAAC;CACxC;AAgBD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,QAAO,OAEgB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,qBAAa,MAAO,SAAQ,MAAM;IAChC;;;OAGG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IAEpC,OAAO,CAAC,UAAU,CAAS;IAElB,aAAa;IAwBb,gBAAgB;IAQzB;;;OAGG;IACH,OAAO,CAAC,WAAW,CA6DjB;CACH"}
|
package/development/router.js
CHANGED
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
* Rebuilt on the Navigation API; see NOTICE.md.
|
|
8
8
|
*/
|
|
9
9
|
import { Routes } from './routes.js';
|
|
10
|
-
// We cache the origin since it can't change
|
|
11
|
-
const origin = location.origin || location.protocol + '//' + location.host;
|
|
12
10
|
const getNavigation = () => window.navigation;
|
|
13
11
|
/**
|
|
14
12
|
* True when the Navigation API is available — Baseline Newly Available since
|
|
@@ -106,10 +104,14 @@ export class Router extends Routes {
|
|
|
106
104
|
_onNavigate = (e) => {
|
|
107
105
|
// Not ours to handle: anything the browser says cannot be intercepted,
|
|
108
106
|
// fragment-only moves, downloads, and POST form submissions.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
//
|
|
108
|
+
// `!= null`, not `!== null`: the spec types both as nullable-but-present,
|
|
109
|
+
// but a polyfill that leaves either unset would make a strict check true
|
|
110
|
+
// for every ordinary link and silently decline the whole app.
|
|
111
|
+
if (!e.canIntercept ||
|
|
112
|
+
e.hashChange ||
|
|
113
|
+
e.downloadRequest != null ||
|
|
114
|
+
e.formData != null) {
|
|
113
115
|
return;
|
|
114
116
|
}
|
|
115
117
|
// Reloads must stay reloads. `canIntercept` is true for them, so without
|
|
@@ -127,8 +129,12 @@ export class Router extends Routes {
|
|
|
127
129
|
if (e.sourceElement?.getAttribute?.('rel') === 'external') {
|
|
128
130
|
return;
|
|
129
131
|
}
|
|
132
|
+
// Read per navigation rather than cached at module scope: the value cannot
|
|
133
|
+
// change, but reading it on import makes merely importing this module throw
|
|
134
|
+
// where there is no `location` (SSR, a bundler evaluating for tree-shaking
|
|
135
|
+
// under the package's `sideEffects: false` claim).
|
|
130
136
|
const url = new URL(e.destination.url);
|
|
131
|
-
if (url.origin !== origin) {
|
|
137
|
+
if (url.origin !== window.location.origin) {
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
134
140
|
// Only intercept what we can actually render. `canIntercept` is true for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Modifications Copyright 2026 VanLandingham Labs, same license.\n * Rebuilt on the Navigation API; see NOTICE.md.\n */\n\nimport {Routes} from './routes.js';\n\n// We cache the origin since it can't change\nconst origin = location.origin || location.protocol + '//' + location.host;\n\n/**\n * The slice of `NavigateEvent` this router reads. Declared locally rather than\n * typing the handler `any`: these properties *are* the correctness boundary, so\n * a typo like `hashchange` for `hashChange` would silently disable a filter\n * forever. Names verified against Chromium's `NavigateEvent.prototype`.\n */\ninterface NavigateEventLike {\n readonly canIntercept: boolean;\n readonly hashChange: boolean;\n readonly downloadRequest: string | null;\n readonly formData: FormData | null;\n readonly navigationType: 'push' | 'replace' | 'reload' | 'traverse';\n readonly signal: AbortSignal;\n readonly destination: {readonly url: string};\n /** Not in every engine yet; used only as a best-effort `rel` check. */\n readonly sourceElement?: Element | null;\n intercept(options: InterceptOptions & {handler?: () => Promise<void>}): void;\n}\n\n/** The subset of `NavigationInterceptOptions` this router forwards. */\nexport interface InterceptOptions {\n focusReset?: 'after-transition' | 'manual';\n scroll?: 'after-transition' | 'manual';\n}\n\ninterface NavigationLike {\n addEventListener(\n type: 'navigate',\n listener: (e: NavigateEventLike) => void\n ): void;\n removeEventListener(\n type: 'navigate',\n listener: (e: NavigateEventLike) => void\n ): void;\n}\n\nconst getNavigation = (): NavigationLike | undefined =>\n (window as unknown as {navigation?: NavigationLike}).navigation;\n\n/**\n * True when the Navigation API is available — Baseline Newly Available since\n * January 2026 (Chrome/Edge, Safari 26.2, Firefox 147).\n *\n * This router **requires** it. Exported so an app can detect an unsupported\n * engine at boot and say so, rather than leaving the user to notice that every\n * link reloads the page.\n */\nexport const supportsNavigationApi = (): boolean =>\n typeof window !== 'undefined' &&\n typeof getNavigation()?.addEventListener === 'function';\n\n/**\n * A root-level router that intercepts navigation via the Navigation API.\n *\n * This class extends Routes so that it can also have a route configuration.\n *\n * There should only be one Router instance on a page, since the Router\n * installs a global listener. Nested routes should be configured with the\n * `Routes` class.\n *\n * ## Why the Navigation API\n *\n * Upstream intercepted navigation with a global click listener plus `popstate`\n * and committed with `history.pushState()`. That is structurally racy:\n * `pushState` is synchronous and `popstate` fires *after* the URL has already\n * moved, but `goto()` awaits `route.enter()` before swapping the outlet. The\n * URL leads and the outlet lags, leaving two sources of truth — the outgoing\n * route re-renders with stale params, and two quick navigations commit in\n * whatever order their `enter()` hooks happen to resolve.\n *\n * `navigateEvent.intercept({handler})` collapses that. The browser commits the\n * URL and holds the navigation un-finished while the handler runs, and it\n * aborts `navigateEvent.signal` when a newer navigation supersedes this one —\n * which `goto()` honours, so a superseded route can no longer win the outlet.\n *\n * ## No legacy fallback\n *\n * An earlier version of this fork kept upstream's click/popstate path for\n * pre-2026 engines. It was removed deliberately. Ten review rounds found\n * divergences between the two paths and **every one was in the click handler**,\n * never in this one — which is structural, not luck: this handler reads a\n * decision the browser has already made, while the click handler had to\n * re-derive it, re-implementing the rules for choosing a navigable, the\n * fragment-navigation predicate, and the modifier-key rules. Each round found\n * another place where the re-implementation and the spec disagreed.\n *\n * On an engine without the API, links fall back to ordinary full page loads.\n * For an app whose server serves the shell on every route that still works —\n * it is slower, not broken — and `supportsNavigationApi()` lets you detect it.\n * If real pre-2026 support is ever needed, use a Navigation API polyfill: one\n * decision path, with compatibility isolated in a layer whose whole job is\n * spec accuracy.\n */\nexport class Router extends Routes {\n /**\n * Options forwarded to `navigateEvent.intercept()`. Leaving these unset\n * gives the browser's default scroll and focus handling.\n */\n interceptOptions?: InterceptOptions;\n\n private _listening = false;\n\n override hostConnected() {\n super.hostConnected();\n // Gated on the exported predicate, not on `navigation !== undefined`:\n // a stub or partial polyfill under that name would otherwise make this\n // branch throw out of connectedCallback while `supportsNavigationApi()`\n // told the app it was unsupported — and then even the initial render below\n // would not run.\n if (supportsNavigationApi()) {\n getNavigation()!.addEventListener('navigate', this._onNavigate);\n this._listening = true;\n }\n // Kick off routed rendering by going to the current URL. Done even without\n // the API: a full page load still renders the right route, which is what\n // makes the unsupported-engine degradation \"slow\" rather than \"blank\".\n // Surfaced rather than left as a bare unhandled rejection, matching the\n // convention in routes.ts: on an engine without the API this is the *only*\n // rendering path, and a deep link with no matching route throws here.\n void this.goto(window.location.pathname).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n }\n\n override hostDisconnected() {\n super.hostDisconnected();\n if (this._listening) {\n getNavigation()?.removeEventListener('navigate', this._onNavigate);\n this._listening = false;\n }\n }\n\n /**\n * Handles same-document navigation from every source at once: anchor clicks,\n * `navigation.navigate()`, `history.pushState()`, and back/forward.\n */\n private _onNavigate = (e: NavigateEventLike) => {\n // Not ours to handle: anything the browser says cannot be intercepted,\n // fragment-only moves, downloads, and POST form submissions.\n if (!e.canIntercept || e.hashChange || e.downloadRequest !== null) {\n return;\n }\n if (e.formData) {\n return;\n }\n\n // Reloads must stay reloads. `canIntercept` is true for them, so without\n // this `location.reload()` silently degrades to re-running goto() on the\n // same path — the document is never replaced, breaking the standard\n // \"new version available, reload\" escape hatch. (It would also disagree\n // with the browser's own refresh button, which is not interceptable.)\n if (e.navigationType === 'reload') {\n return;\n }\n\n // `rel=\"external\"` is a convention this router honours — it is not defined\n // by HTML or by the Navigation API, so the browser will not decline these\n // for us. Best-effort: `sourceElement` is not in every engine, and is\n // absent for programmatic navigation.\n if (e.sourceElement?.getAttribute?.('rel') === 'external') {\n return;\n }\n\n const url = new URL(e.destination.url);\n if (url.origin !== origin) {\n return;\n }\n\n // Only intercept what we can actually render. `canIntercept` is true for\n // any same-origin URL, including cross-document ones — so without this a\n // link to a server-rendered page, an export endpoint, or a GET form\n // (whose `formData` is null) gets swallowed: the URL commits, goto()\n // throws \"No route found\", and the address bar is left pointing somewhere\n // the outlet never went. Declining lets the browser do the real\n // navigation, which is the correct outcome.\n if (!this.hasRouteFor(url.pathname)) {\n return;\n }\n\n e.intercept({\n ...this.interceptOptions,\n handler: async () => {\n // `e.signal` aborts if another navigation starts before this handler\n // resolves; goto() checks it after `enter()` and stands down.\n await this.goto(url.pathname, {signal: e.signal});\n },\n });\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license\n * Copyright 2021 Google LLC\n * SPDX-License-Identifier: BSD-3-Clause\n *\n * Modifications Copyright 2026 VanLandingham Labs, same license.\n * Rebuilt on the Navigation API; see NOTICE.md.\n */\n\nimport {Routes} from './routes.js';\n\n/**\n * The slice of `NavigateEvent` this router reads. Declared locally rather than\n * typing the handler `any`: these properties *are* the correctness boundary, so\n * a typo like `hashchange` for `hashChange` would silently disable a filter\n * forever. Names verified against Chromium's `NavigateEvent.prototype`.\n */\ninterface NavigateEventLike {\n readonly canIntercept: boolean;\n readonly hashChange: boolean;\n readonly downloadRequest: string | null;\n readonly formData: FormData | null;\n readonly navigationType: 'push' | 'replace' | 'reload' | 'traverse';\n readonly signal: AbortSignal;\n readonly destination: {readonly url: string};\n /** Not in every engine yet; used only as a best-effort `rel` check. */\n readonly sourceElement?: Element | null;\n intercept(options: InterceptOptions & {handler?: () => Promise<void>}): void;\n}\n\n/** The subset of `NavigationInterceptOptions` this router forwards. */\nexport interface InterceptOptions {\n focusReset?: 'after-transition' | 'manual';\n scroll?: 'after-transition' | 'manual';\n}\n\ninterface NavigationLike {\n addEventListener(\n type: 'navigate',\n listener: (e: NavigateEventLike) => void\n ): void;\n removeEventListener(\n type: 'navigate',\n listener: (e: NavigateEventLike) => void\n ): void;\n}\n\nconst getNavigation = (): NavigationLike | undefined =>\n (window as unknown as {navigation?: NavigationLike}).navigation;\n\n/**\n * True when the Navigation API is available — Baseline Newly Available since\n * January 2026 (Chrome/Edge, Safari 26.2, Firefox 147).\n *\n * This router **requires** it. Exported so an app can detect an unsupported\n * engine at boot and say so, rather than leaving the user to notice that every\n * link reloads the page.\n */\nexport const supportsNavigationApi = (): boolean =>\n typeof window !== 'undefined' &&\n typeof getNavigation()?.addEventListener === 'function';\n\n/**\n * A root-level router that intercepts navigation via the Navigation API.\n *\n * This class extends Routes so that it can also have a route configuration.\n *\n * There should only be one Router instance on a page, since the Router\n * installs a global listener. Nested routes should be configured with the\n * `Routes` class.\n *\n * ## Why the Navigation API\n *\n * Upstream intercepted navigation with a global click listener plus `popstate`\n * and committed with `history.pushState()`. That is structurally racy:\n * `pushState` is synchronous and `popstate` fires *after* the URL has already\n * moved, but `goto()` awaits `route.enter()` before swapping the outlet. The\n * URL leads and the outlet lags, leaving two sources of truth — the outgoing\n * route re-renders with stale params, and two quick navigations commit in\n * whatever order their `enter()` hooks happen to resolve.\n *\n * `navigateEvent.intercept({handler})` collapses that. The browser commits the\n * URL and holds the navigation un-finished while the handler runs, and it\n * aborts `navigateEvent.signal` when a newer navigation supersedes this one —\n * which `goto()` honours, so a superseded route can no longer win the outlet.\n *\n * ## No legacy fallback\n *\n * An earlier version of this fork kept upstream's click/popstate path for\n * pre-2026 engines. It was removed deliberately. Ten review rounds found\n * divergences between the two paths and **every one was in the click handler**,\n * never in this one — which is structural, not luck: this handler reads a\n * decision the browser has already made, while the click handler had to\n * re-derive it, re-implementing the rules for choosing a navigable, the\n * fragment-navigation predicate, and the modifier-key rules. Each round found\n * another place where the re-implementation and the spec disagreed.\n *\n * On an engine without the API, links fall back to ordinary full page loads.\n * For an app whose server serves the shell on every route that still works —\n * it is slower, not broken — and `supportsNavigationApi()` lets you detect it.\n * If real pre-2026 support is ever needed, use a Navigation API polyfill: one\n * decision path, with compatibility isolated in a layer whose whole job is\n * spec accuracy.\n */\nexport class Router extends Routes {\n /**\n * Options forwarded to `navigateEvent.intercept()`. Leaving these unset\n * gives the browser's default scroll and focus handling.\n */\n interceptOptions?: InterceptOptions;\n\n private _listening = false;\n\n override hostConnected() {\n super.hostConnected();\n // Gated on the exported predicate, not on `navigation !== undefined`:\n // a stub or partial polyfill under that name would otherwise make this\n // branch throw out of connectedCallback while `supportsNavigationApi()`\n // told the app it was unsupported — and then even the initial render below\n // would not run.\n if (supportsNavigationApi()) {\n getNavigation()!.addEventListener('navigate', this._onNavigate);\n this._listening = true;\n }\n // Kick off routed rendering by going to the current URL. Done even without\n // the API: a full page load still renders the right route, which is what\n // makes the unsupported-engine degradation \"slow\" rather than \"blank\".\n // Surfaced rather than left as a bare unhandled rejection, matching the\n // convention in routes.ts: on an engine without the API this is the *only*\n // rendering path, and a deep link with no matching route throws here.\n void this.goto(window.location.pathname).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\n }\n\n override hostDisconnected() {\n super.hostDisconnected();\n if (this._listening) {\n getNavigation()?.removeEventListener('navigate', this._onNavigate);\n this._listening = false;\n }\n }\n\n /**\n * Handles same-document navigation from every source at once: anchor clicks,\n * `navigation.navigate()`, `history.pushState()`, and back/forward.\n */\n private _onNavigate = (e: NavigateEventLike) => {\n // Not ours to handle: anything the browser says cannot be intercepted,\n // fragment-only moves, downloads, and POST form submissions.\n //\n // `!= null`, not `!== null`: the spec types both as nullable-but-present,\n // but a polyfill that leaves either unset would make a strict check true\n // for every ordinary link and silently decline the whole app.\n if (\n !e.canIntercept ||\n e.hashChange ||\n e.downloadRequest != null ||\n e.formData != null\n ) {\n return;\n }\n\n // Reloads must stay reloads. `canIntercept` is true for them, so without\n // this `location.reload()` silently degrades to re-running goto() on the\n // same path — the document is never replaced, breaking the standard\n // \"new version available, reload\" escape hatch. (It would also disagree\n // with the browser's own refresh button, which is not interceptable.)\n if (e.navigationType === 'reload') {\n return;\n }\n\n // `rel=\"external\"` is a convention this router honours — it is not defined\n // by HTML or by the Navigation API, so the browser will not decline these\n // for us. Best-effort: `sourceElement` is not in every engine, and is\n // absent for programmatic navigation.\n if (e.sourceElement?.getAttribute?.('rel') === 'external') {\n return;\n }\n\n // Read per navigation rather than cached at module scope: the value cannot\n // change, but reading it on import makes merely importing this module throw\n // where there is no `location` (SSR, a bundler evaluating for tree-shaking\n // under the package's `sideEffects: false` claim).\n const url = new URL(e.destination.url);\n if (url.origin !== window.location.origin) {\n return;\n }\n\n // Only intercept what we can actually render. `canIntercept` is true for\n // any same-origin URL, including cross-document ones — so without this a\n // link to a server-rendered page, an export endpoint, or a GET form\n // (whose `formData` is null) gets swallowed: the URL commits, goto()\n // throws \"No route found\", and the address bar is left pointing somewhere\n // the outlet never went. Declining lets the browser do the real\n // navigation, which is the correct outcome.\n if (!this.hasRouteFor(url.pathname)) {\n return;\n }\n\n e.intercept({\n ...this.interceptOptions,\n handler: async () => {\n // `e.signal` aborts if another navigation starts before this handler\n // resolves; goto() checks it after `enter()` and stands down.\n await this.goto(url.pathname, {signal: e.signal});\n },\n });\n };\n}\n"]}
|
package/development/routes.d.ts
CHANGED
|
@@ -122,6 +122,27 @@ export declare class Routes implements ReactiveController {
|
|
|
122
122
|
get params(): {
|
|
123
123
|
[key: string]: string | undefined;
|
|
124
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* Hands a tail match to a child controller. Shared by the propagation loop in
|
|
127
|
+
* `goto()` and the late-mount path in `_onRoutesConnected`, so that identical
|
|
128
|
+
* input cannot be silent on one and an uncaught global throw on the other.
|
|
129
|
+
*
|
|
130
|
+
* A child with no route for the new tail is the expected case, not an error —
|
|
131
|
+
* the outgoing branch mid-swap, or a deep link to a path the child cannot
|
|
132
|
+
* render. Filtered structurally rather than by swallowing every rejection, so
|
|
133
|
+
* a genuine `enter()` rejection still surfaces the way it does upstream.
|
|
134
|
+
* Skipping must still supersede: `goto()` is where the counter is bumped, so
|
|
135
|
+
* returning without it would leave an in-flight child navigation current,
|
|
136
|
+
* free to commit over a URL that has moved on.
|
|
137
|
+
*
|
|
138
|
+
* No abort signal is threaded through, and the goto is deliberately not
|
|
139
|
+
* awaited. The parent commits its own state before children run, so a child
|
|
140
|
+
* handed an already-aborted signal stands down with no newer goto() arriving
|
|
141
|
+
* to correct it, leaving the nested outlet stuck — reachable, because a
|
|
142
|
+
* hash-only navigation aborts the outstanding one without producing a
|
|
143
|
+
* replacement. Supersession is the counter's job.
|
|
144
|
+
*/
|
|
145
|
+
private _routeChild;
|
|
125
146
|
/**
|
|
126
147
|
* Invalidate any in-flight `goto()` on this controller without starting a
|
|
127
148
|
* new one. Same-class access, so `_gotoSeq` stays private to `Routes`.
|
|
@@ -138,9 +159,14 @@ export declare class Routes implements ReactiveController {
|
|
|
138
159
|
*/
|
|
139
160
|
hasRouteFor(pathname: string): boolean;
|
|
140
161
|
/**
|
|
141
|
-
* Matches `
|
|
162
|
+
* Matches `pathname` against the installed routes and returns the first match
|
|
163
|
+
* with its parsed parameters, or the fallback's match if one is configured.
|
|
164
|
+
*
|
|
165
|
+
* One `exec()` per candidate rather than `test()` to select and `exec()` to
|
|
166
|
+
* extract: that ran the winning pattern twice, and every caller that wants a
|
|
167
|
+
* route wants its params too.
|
|
142
168
|
*/
|
|
143
|
-
private
|
|
169
|
+
private _match;
|
|
144
170
|
hostConnected(): void;
|
|
145
171
|
hostDisconnected(): void;
|
|
146
172
|
private _onRoutesConnected;
|
|
@@ -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,6 +21,11 @@ const getPattern = (route) => {
|
|
|
21
21
|
}
|
|
22
22
|
return pattern;
|
|
23
23
|
};
|
|
24
|
+
// The implicit "/*" pattern every configured fallback matches against. Built on
|
|
25
|
+
// first use rather than at module scope so that importing this module never
|
|
26
|
+
// touches `URLPattern` — it may be polyfilled after import, or absent entirely.
|
|
27
|
+
let wildcardPattern;
|
|
28
|
+
const getWildcardPattern = () => (wildcardPattern ??= new URLPattern({ pathname: '/*' }));
|
|
24
29
|
/**
|
|
25
30
|
* A reactive controller that performs location-based routing using a
|
|
26
31
|
* configuration of URL patterns and associated render callbacks.
|
|
@@ -114,7 +119,6 @@ export class Routes {
|
|
|
114
119
|
// fragments. It currently only handles path names because it's easier to
|
|
115
120
|
// completely disregard the origin for now. The click handler only does
|
|
116
121
|
// an in-page navigation if the origin matches anyway.
|
|
117
|
-
const signal = options?.signal;
|
|
118
122
|
// Last-goto-wins, per controller. The navigation signal alone is not
|
|
119
123
|
// enough: a child controller mounts as a *result* of its parent's render,
|
|
120
124
|
// so its first goto() comes from `_onRoutesConnected` — after the parent's
|
|
@@ -123,7 +127,6 @@ export class Routes {
|
|
|
123
127
|
// newer one. This also keeps `Routes` correct when used on its own, with
|
|
124
128
|
// no `Router` and no Navigation API in the picture.
|
|
125
129
|
const seq = ++this._gotoSeq;
|
|
126
|
-
const superseded = () => signal?.aborted === true || seq !== this._gotoSeq;
|
|
127
130
|
let tailGroup;
|
|
128
131
|
if (this.routes.length === 0 && this.fallback === undefined) {
|
|
129
132
|
// If a routes controller has none of its own routes it acts like it has
|
|
@@ -135,13 +138,11 @@ export class Routes {
|
|
|
135
138
|
this._currentParams = { 0: tailGroup };
|
|
136
139
|
}
|
|
137
140
|
else {
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
141
|
+
const match = this._match(pathname);
|
|
142
|
+
if (match === undefined) {
|
|
140
143
|
throw new Error(`No route found for ${pathname}`);
|
|
141
144
|
}
|
|
142
|
-
const
|
|
143
|
-
const result = pattern.exec({ pathname });
|
|
144
|
-
const params = result?.pathname.groups ?? {};
|
|
145
|
+
const { route, params } = match;
|
|
145
146
|
tailGroup = getTailGroup(params);
|
|
146
147
|
if (typeof route.enter === 'function') {
|
|
147
148
|
const success = await route.enter(params);
|
|
@@ -152,7 +153,7 @@ export class Routes {
|
|
|
152
153
|
}
|
|
153
154
|
// A newer navigation superseded this one while `enter` was awaiting.
|
|
154
155
|
// Committing now would swap the outlet onto a route the URL has left.
|
|
155
|
-
if (
|
|
156
|
+
if (options?.signal?.aborted === true || seq !== this._gotoSeq) {
|
|
156
157
|
return;
|
|
157
158
|
}
|
|
158
159
|
// Only update route state if the enter handler completes successfully
|
|
@@ -175,34 +176,10 @@ export class Routes {
|
|
|
175
176
|
// propagates out of here and `requestUpdate()` below never runs — URL
|
|
176
177
|
// committed, outlet stranded, i.e. this fork's own thesis bug one level
|
|
177
178
|
// down. Nested supersession is handled by the goto counter above, not by
|
|
178
|
-
// awaiting.
|
|
179
|
+
// awaiting. `_routeChild` covers the per-child filtering and error policy.
|
|
179
180
|
if (tailGroup !== undefined) {
|
|
180
181
|
for (const childRoutes of this._childRoutes) {
|
|
181
|
-
|
|
182
|
-
// parent commits before children run, so a child handed an aborted
|
|
183
|
-
// signal stands down with no newer goto() arriving to correct it,
|
|
184
|
-
// leaving the nested outlet stuck. A hash-only navigation aborts the
|
|
185
|
-
// outstanding one without producing a replacement, so this is
|
|
186
|
-
// reachable. Supersession is the counter's job.
|
|
187
|
-
//
|
|
188
|
-
// The expected failure here is a child with no route for the new tail
|
|
189
|
-
// — the outgoing branch, mid-swap. Filter that structurally rather
|
|
190
|
-
// than swallowing everything, so a genuine `enter()` rejection still
|
|
191
|
-
// surfaces the way it does upstream instead of vanishing.
|
|
192
|
-
if (!childRoutes.hasRouteFor(tailGroup)) {
|
|
193
|
-
// Skip the navigation but still supersede: `goto()` is where the
|
|
194
|
-
// counter is bumped, so returning early here would leave an
|
|
195
|
-
// in-flight child navigation current, free to commit over a URL that
|
|
196
|
-
// has moved on. Removing the abort signal above is only safe because
|
|
197
|
-
// the counter always runs — including here.
|
|
198
|
-
childRoutes._supersede();
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
void childRoutes.goto(tailGroup).catch((err) => {
|
|
202
|
-
queueMicrotask(() => {
|
|
203
|
-
throw err;
|
|
204
|
-
});
|
|
205
|
-
});
|
|
182
|
+
this._routeChild(childRoutes, tailGroup);
|
|
206
183
|
}
|
|
207
184
|
}
|
|
208
185
|
this._host.requestUpdate();
|
|
@@ -219,6 +196,37 @@ export class Routes {
|
|
|
219
196
|
get params() {
|
|
220
197
|
return this._currentParams;
|
|
221
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Hands a tail match to a child controller. Shared by the propagation loop in
|
|
201
|
+
* `goto()` and the late-mount path in `_onRoutesConnected`, so that identical
|
|
202
|
+
* input cannot be silent on one and an uncaught global throw on the other.
|
|
203
|
+
*
|
|
204
|
+
* A child with no route for the new tail is the expected case, not an error —
|
|
205
|
+
* the outgoing branch mid-swap, or a deep link to a path the child cannot
|
|
206
|
+
* render. Filtered structurally rather than by swallowing every rejection, so
|
|
207
|
+
* a genuine `enter()` rejection still surfaces the way it does upstream.
|
|
208
|
+
* Skipping must still supersede: `goto()` is where the counter is bumped, so
|
|
209
|
+
* returning without it would leave an in-flight child navigation current,
|
|
210
|
+
* free to commit over a URL that has moved on.
|
|
211
|
+
*
|
|
212
|
+
* No abort signal is threaded through, and the goto is deliberately not
|
|
213
|
+
* awaited. The parent commits its own state before children run, so a child
|
|
214
|
+
* handed an already-aborted signal stands down with no newer goto() arriving
|
|
215
|
+
* to correct it, leaving the nested outlet stuck — reachable, because a
|
|
216
|
+
* hash-only navigation aborts the outstanding one without producing a
|
|
217
|
+
* replacement. Supersession is the counter's job.
|
|
218
|
+
*/
|
|
219
|
+
_routeChild(child, tail) {
|
|
220
|
+
if (!child.hasRouteFor(tail)) {
|
|
221
|
+
child._supersede();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
void child.goto(tail).catch((err) => {
|
|
225
|
+
queueMicrotask(() => {
|
|
226
|
+
throw err;
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
222
230
|
/**
|
|
223
231
|
* Invalidate any in-flight `goto()` on this controller without starting a
|
|
224
232
|
* new one. Same-class access, so `_gotoSeq` stays private to `Routes`.
|
|
@@ -253,27 +261,45 @@ export class Routes {
|
|
|
253
261
|
* server-rendered page, an export endpoint, or a GET form still works.
|
|
254
262
|
*/
|
|
255
263
|
hasRouteFor(pathname) {
|
|
256
|
-
//
|
|
257
|
-
// behaves as if it had a single `/*` route.
|
|
258
|
-
|
|
264
|
+
// A fallback matches everything, and a controller with no routes of its own
|
|
265
|
+
// behaves as if it had a single `/*` route (goto()'s special case). Either
|
|
266
|
+
// way the answer is yes without running a single pattern — worth
|
|
267
|
+
// short-circuiting, since `Router` asks this on every navigation.
|
|
268
|
+
if (this.fallback !== undefined || this.routes.length === 0) {
|
|
259
269
|
return true;
|
|
260
270
|
}
|
|
261
|
-
|
|
271
|
+
// `test()`, not `_match()`: this only needs the yes/no, and `exec()` pays
|
|
272
|
+
// ~8x on a hit to build a groups object the caller would throw away.
|
|
273
|
+
return this.routes.some((r) => getPattern(r).test({ pathname }));
|
|
262
274
|
}
|
|
263
275
|
/**
|
|
264
|
-
* Matches `
|
|
276
|
+
* Matches `pathname` against the installed routes and returns the first match
|
|
277
|
+
* with its parsed parameters, or the fallback's match if one is configured.
|
|
278
|
+
*
|
|
279
|
+
* One `exec()` per candidate rather than `test()` to select and `exec()` to
|
|
280
|
+
* extract: that ran the winning pattern twice, and every caller that wants a
|
|
281
|
+
* route wants its params too.
|
|
265
282
|
*/
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
283
|
+
_match(pathname) {
|
|
284
|
+
for (const route of this.routes) {
|
|
285
|
+
const result = getPattern(route).exec({ pathname });
|
|
286
|
+
if (result !== null) {
|
|
287
|
+
return { route, params: result.pathname.groups };
|
|
288
|
+
}
|
|
270
289
|
}
|
|
271
|
-
if (this.fallback) {
|
|
272
|
-
|
|
273
|
-
// the public API but is added here to return a valid RouteConfig.
|
|
274
|
-
return { ...this.fallback, path: '/*' };
|
|
290
|
+
if (this.fallback === undefined) {
|
|
291
|
+
return undefined;
|
|
275
292
|
}
|
|
276
|
-
|
|
293
|
+
// The fallback route behaves like it has a "/*" path. This is hidden from
|
|
294
|
+
// the public API but is added here to return a valid RouteConfig. The
|
|
295
|
+
// pattern is the shared one rather than one derived from this object:
|
|
296
|
+
// the spread produces a fresh object every call, which `patternCache` —
|
|
297
|
+
// keyed by identity — would miss, rebuilding a URLPattern per navigation.
|
|
298
|
+
const wildcard = getWildcardPattern();
|
|
299
|
+
return {
|
|
300
|
+
route: { ...this.fallback, path: '/*' },
|
|
301
|
+
params: wildcard.exec({ pathname })?.pathname.groups ?? {},
|
|
302
|
+
};
|
|
277
303
|
}
|
|
278
304
|
hostConnected() {
|
|
279
305
|
this._host.addEventListener(RoutesConnectedEvent.eventName, this._onRoutesConnected);
|
|
@@ -306,27 +332,16 @@ export class Routes {
|
|
|
306
332
|
childRoutes._parentRoutes = this;
|
|
307
333
|
e.stopImmediatePropagation();
|
|
308
334
|
e.onDisconnect = () => {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
335
|
+
const index = this._childRoutes.indexOf(childRoutes);
|
|
336
|
+
if (index !== -1) {
|
|
337
|
+
this._childRoutes.splice(index, 1);
|
|
338
|
+
}
|
|
312
339
|
};
|
|
340
|
+
// A child that mounts under an existing tail match has to be caught up to
|
|
341
|
+
// it — it missed the propagation loop in goto() that ran before it existed.
|
|
313
342
|
const tailGroup = getTailGroup(this._currentParams);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// to `/x/unknown`), not an error. Without this the two call sites disagree
|
|
317
|
-
// — silent there, uncaught global throw here — for identical input.
|
|
318
|
-
if (tailGroup !== undefined && childRoutes.hasRouteFor(tailGroup)) {
|
|
319
|
-
// No signal here on purpose. The parent commits its own state before
|
|
320
|
-
// children run, so by the time a late child mounts the navigation may
|
|
321
|
-
// already have been aborted — handing it that signal makes it stand down
|
|
322
|
-
// with no newer goto() ever arriving to correct it, leaving the nested
|
|
323
|
-
// outlet blank permanently. The goto counter covers what matters
|
|
324
|
-
// (supersession by a newer goto).
|
|
325
|
-
void childRoutes.goto(tailGroup).catch((err) => {
|
|
326
|
-
queueMicrotask(() => {
|
|
327
|
-
throw err;
|
|
328
|
-
});
|
|
329
|
-
});
|
|
343
|
+
if (tailGroup !== undefined) {
|
|
344
|
+
this._routeChild(childRoutes, tailGroup);
|
|
330
345
|
}
|
|
331
346
|
};
|
|
332
347
|
}
|
|
@@ -335,13 +350,29 @@ export class Routes {
|
|
|
335
350
|
* wildcard at the end of a pathname pattern, like `/foo/*`
|
|
336
351
|
*/
|
|
337
352
|
const getTailGroup = (groups) => {
|
|
338
|
-
let
|
|
353
|
+
let tailIndex = -1;
|
|
339
354
|
for (const key of Object.keys(groups)) {
|
|
340
|
-
|
|
341
|
-
|
|
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;
|
|
342
373
|
}
|
|
343
374
|
}
|
|
344
|
-
return
|
|
375
|
+
return tailIndex < 0 ? undefined : groups[String(tailIndex)];
|
|
345
376
|
};
|
|
346
377
|
/**
|
|
347
378
|
* This event is fired from Routes controllers when their host is connected to
|
|
@@ -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/**\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 const signal = options?.signal;\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 const superseded = () => signal?.aborted === true || 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 route = this._getRoute(pathname);\n if (route === undefined) {\n throw new Error(`No route found for ${pathname}`);\n }\n const pattern = getPattern(route);\n const result = pattern.exec({pathname});\n const params = result?.pathname.groups ?? {};\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 (superseded()) {\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. Errors are swallowed rather than left as unhandled rejections.\n if (tailGroup !== undefined) {\n for (const childRoutes of this._childRoutes) {\n // No signal, for the same reason as the late-mount path below: the\n // parent commits before children run, so a child handed an aborted\n // signal stands down with no newer goto() arriving to correct it,\n // leaving the nested outlet stuck. A hash-only navigation aborts the\n // outstanding one without producing a replacement, so this is\n // reachable. Supersession is the counter's job.\n //\n // The expected failure here is a child with no route for the new tail\n // — the outgoing branch, mid-swap. Filter that structurally rather\n // than swallowing everything, so a genuine `enter()` rejection still\n // surfaces the way it does upstream instead of vanishing.\n if (!childRoutes.hasRouteFor(tailGroup)) {\n // Skip the navigation but still supersede: `goto()` is where the\n // counter is bumped, so returning early here would leave an\n // in-flight child navigation current, free to commit over a URL that\n // has moved on. Removing the abort signal above is only safe because\n // the counter always runs — including here.\n childRoutes._supersede();\n continue;\n }\n void childRoutes.goto(tailGroup).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\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 * 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 // Mirrors goto()'s special case: a controller with no routes of its own\n // behaves as if it had a single `/*` route.\n if (this.routes.length === 0 && this.fallback === undefined) {\n return true;\n }\n return this._getRoute(pathname) !== undefined;\n }\n\n /**\n * Matches `url` against the installed routes and returns the first match.\n */\n private _getRoute(pathname: string): RouteConfig | undefined {\n const matchedRoute = this.routes.find((r) =>\n getPattern(r).test({pathname: pathname})\n );\n if (matchedRoute || this.fallback === undefined) {\n return matchedRoute;\n }\n if (this.fallback) {\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.\n return {...this.fallback, path: '/*'};\n }\n return undefined;\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 // Remove route from this._childRoutes:\n // `>>> 0` converts -1 to 2**32-1\n this._childRoutes?.splice(\n this._childRoutes.indexOf(childRoutes) >>> 0,\n 1\n );\n };\n\n const tailGroup = getTailGroup(this._currentParams);\n // Same structural filter as the propagation path in goto(): a child that\n // mounts under a tail it cannot render is the expected case (a deep link\n // to `/x/unknown`), not an error. Without this the two call sites disagree\n // — silent there, uncaught global throw here — for identical input.\n if (tailGroup !== undefined && childRoutes.hasRouteFor(tailGroup)) {\n // No signal here on purpose. The parent commits its own state before\n // children run, so by the time a late child mounts the navigation may\n // already have been aborted — handing it that signal makes it stand down\n // with no newer goto() ever arriving to correct it, leaving the nested\n // outlet blank permanently. The goto counter covers what matters\n // (supersession by a newer goto).\n void childRoutes.goto(tailGroup).catch((err) => {\n queueMicrotask(() => {\n throw err;\n });\n });\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 tailKey: string | undefined;\n for (const key of Object.keys(groups)) {\n if (/\\d+/.test(key) && (tailKey === undefined || key > tailKey!)) {\n tailKey = key;\n }\n }\n return tailKey && groups[tailKey];\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 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lit-navigation-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A router for Lit built on the Navigation API. Fork of @lit-labs/router.",
|
|
5
5
|
"license": "BSD-3-Clause",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"!/development/test/",
|
|
32
32
|
"/src/",
|
|
33
33
|
"!/src/test/",
|
|
34
|
-
"/NOTICE.md"
|
|
34
|
+
"/NOTICE.md",
|
|
35
|
+
"/CHANGELOG.md"
|
|
35
36
|
],
|
|
36
37
|
"scripts": {
|
|
37
38
|
"prepare": "npm run build",
|
package/src/router.ts
CHANGED
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
import {Routes} from './routes.js';
|
|
11
11
|
|
|
12
|
-
// We cache the origin since it can't change
|
|
13
|
-
const origin = location.origin || location.protocol + '//' + location.host;
|
|
14
|
-
|
|
15
12
|
/**
|
|
16
13
|
* The slice of `NavigateEvent` this router reads. Declared locally rather than
|
|
17
14
|
* typing the handler `any`: these properties *are* the correctness boundary, so
|
|
@@ -153,10 +150,16 @@ export class Router extends Routes {
|
|
|
153
150
|
private _onNavigate = (e: NavigateEventLike) => {
|
|
154
151
|
// Not ours to handle: anything the browser says cannot be intercepted,
|
|
155
152
|
// fragment-only moves, downloads, and POST form submissions.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
//
|
|
154
|
+
// `!= null`, not `!== null`: the spec types both as nullable-but-present,
|
|
155
|
+
// but a polyfill that leaves either unset would make a strict check true
|
|
156
|
+
// for every ordinary link and silently decline the whole app.
|
|
157
|
+
if (
|
|
158
|
+
!e.canIntercept ||
|
|
159
|
+
e.hashChange ||
|
|
160
|
+
e.downloadRequest != null ||
|
|
161
|
+
e.formData != null
|
|
162
|
+
) {
|
|
160
163
|
return;
|
|
161
164
|
}
|
|
162
165
|
|
|
@@ -177,8 +180,12 @@ export class Router extends Routes {
|
|
|
177
180
|
return;
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
// Read per navigation rather than cached at module scope: the value cannot
|
|
184
|
+
// change, but reading it on import makes merely importing this module throw
|
|
185
|
+
// where there is no `location` (SSR, a bundler evaluating for tree-shaking
|
|
186
|
+
// under the package's `sideEffects: false` claim).
|
|
180
187
|
const url = new URL(e.destination.url);
|
|
181
|
-
if (url.origin !== origin) {
|
|
188
|
+
if (url.origin !== window.location.origin) {
|
|
182
189
|
return;
|
|
183
190
|
}
|
|
184
191
|
|
package/src/routes.ts
CHANGED
|
@@ -81,6 +81,14 @@ const getPattern = (route: RouteConfig): URLPatternLike => {
|
|
|
81
81
|
return pattern;
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
// The implicit "/*" pattern every configured fallback matches against. Built on
|
|
85
|
+
// first use rather than at module scope so that importing this module never
|
|
86
|
+
// touches `URLPattern` — it may be polyfilled after import, or absent entirely.
|
|
87
|
+
let wildcardPattern: URLPatternLike | undefined;
|
|
88
|
+
|
|
89
|
+
const getWildcardPattern = (): URLPatternLike =>
|
|
90
|
+
(wildcardPattern ??= new URLPattern({pathname: '/*'}));
|
|
91
|
+
|
|
84
92
|
/**
|
|
85
93
|
* A reactive controller that performs location-based routing using a
|
|
86
94
|
* configuration of URL patterns and associated render callbacks.
|
|
@@ -191,7 +199,6 @@ export class Routes implements ReactiveController {
|
|
|
191
199
|
// fragments. It currently only handles path names because it's easier to
|
|
192
200
|
// completely disregard the origin for now. The click handler only does
|
|
193
201
|
// an in-page navigation if the origin matches anyway.
|
|
194
|
-
const signal = options?.signal;
|
|
195
202
|
// Last-goto-wins, per controller. The navigation signal alone is not
|
|
196
203
|
// enough: a child controller mounts as a *result* of its parent's render,
|
|
197
204
|
// so its first goto() comes from `_onRoutesConnected` — after the parent's
|
|
@@ -200,7 +207,6 @@ export class Routes implements ReactiveController {
|
|
|
200
207
|
// newer one. This also keeps `Routes` correct when used on its own, with
|
|
201
208
|
// no `Router` and no Navigation API in the picture.
|
|
202
209
|
const seq = ++this._gotoSeq;
|
|
203
|
-
const superseded = () => signal?.aborted === true || seq !== this._gotoSeq;
|
|
204
210
|
let tailGroup: string | undefined;
|
|
205
211
|
|
|
206
212
|
if (this.routes.length === 0 && this.fallback === undefined) {
|
|
@@ -212,13 +218,11 @@ export class Routes implements ReactiveController {
|
|
|
212
218
|
// Simulate a tail group with the whole pathname
|
|
213
219
|
this._currentParams = {0: tailGroup};
|
|
214
220
|
} else {
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
221
|
+
const match = this._match(pathname);
|
|
222
|
+
if (match === undefined) {
|
|
217
223
|
throw new Error(`No route found for ${pathname}`);
|
|
218
224
|
}
|
|
219
|
-
const
|
|
220
|
-
const result = pattern.exec({pathname});
|
|
221
|
-
const params = result?.pathname.groups ?? {};
|
|
225
|
+
const {route, params} = match;
|
|
222
226
|
tailGroup = getTailGroup(params);
|
|
223
227
|
if (typeof route.enter === 'function') {
|
|
224
228
|
const success = await route.enter(params);
|
|
@@ -229,7 +233,7 @@ export class Routes implements ReactiveController {
|
|
|
229
233
|
}
|
|
230
234
|
// A newer navigation superseded this one while `enter` was awaiting.
|
|
231
235
|
// Committing now would swap the outlet onto a route the URL has left.
|
|
232
|
-
if (
|
|
236
|
+
if (options?.signal?.aborted === true || seq !== this._gotoSeq) {
|
|
233
237
|
return;
|
|
234
238
|
}
|
|
235
239
|
// Only update route state if the enter handler completes successfully
|
|
@@ -253,34 +257,10 @@ export class Routes implements ReactiveController {
|
|
|
253
257
|
// propagates out of here and `requestUpdate()` below never runs — URL
|
|
254
258
|
// committed, outlet stranded, i.e. this fork's own thesis bug one level
|
|
255
259
|
// down. Nested supersession is handled by the goto counter above, not by
|
|
256
|
-
// awaiting.
|
|
260
|
+
// awaiting. `_routeChild` covers the per-child filtering and error policy.
|
|
257
261
|
if (tailGroup !== undefined) {
|
|
258
262
|
for (const childRoutes of this._childRoutes) {
|
|
259
|
-
|
|
260
|
-
// parent commits before children run, so a child handed an aborted
|
|
261
|
-
// signal stands down with no newer goto() arriving to correct it,
|
|
262
|
-
// leaving the nested outlet stuck. A hash-only navigation aborts the
|
|
263
|
-
// outstanding one without producing a replacement, so this is
|
|
264
|
-
// reachable. Supersession is the counter's job.
|
|
265
|
-
//
|
|
266
|
-
// The expected failure here is a child with no route for the new tail
|
|
267
|
-
// — the outgoing branch, mid-swap. Filter that structurally rather
|
|
268
|
-
// than swallowing everything, so a genuine `enter()` rejection still
|
|
269
|
-
// surfaces the way it does upstream instead of vanishing.
|
|
270
|
-
if (!childRoutes.hasRouteFor(tailGroup)) {
|
|
271
|
-
// Skip the navigation but still supersede: `goto()` is where the
|
|
272
|
-
// counter is bumped, so returning early here would leave an
|
|
273
|
-
// in-flight child navigation current, free to commit over a URL that
|
|
274
|
-
// has moved on. Removing the abort signal above is only safe because
|
|
275
|
-
// the counter always runs — including here.
|
|
276
|
-
childRoutes._supersede();
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
void childRoutes.goto(tailGroup).catch((err) => {
|
|
280
|
-
queueMicrotask(() => {
|
|
281
|
-
throw err;
|
|
282
|
-
});
|
|
283
|
-
});
|
|
263
|
+
this._routeChild(childRoutes, tailGroup);
|
|
284
264
|
}
|
|
285
265
|
}
|
|
286
266
|
this._host.requestUpdate();
|
|
@@ -300,6 +280,38 @@ export class Routes implements ReactiveController {
|
|
|
300
280
|
return this._currentParams;
|
|
301
281
|
}
|
|
302
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Hands a tail match to a child controller. Shared by the propagation loop in
|
|
285
|
+
* `goto()` and the late-mount path in `_onRoutesConnected`, so that identical
|
|
286
|
+
* input cannot be silent on one and an uncaught global throw on the other.
|
|
287
|
+
*
|
|
288
|
+
* A child with no route for the new tail is the expected case, not an error —
|
|
289
|
+
* the outgoing branch mid-swap, or a deep link to a path the child cannot
|
|
290
|
+
* render. Filtered structurally rather than by swallowing every rejection, so
|
|
291
|
+
* a genuine `enter()` rejection still surfaces the way it does upstream.
|
|
292
|
+
* Skipping must still supersede: `goto()` is where the counter is bumped, so
|
|
293
|
+
* returning without it would leave an in-flight child navigation current,
|
|
294
|
+
* free to commit over a URL that has moved on.
|
|
295
|
+
*
|
|
296
|
+
* No abort signal is threaded through, and the goto is deliberately not
|
|
297
|
+
* awaited. The parent commits its own state before children run, so a child
|
|
298
|
+
* handed an already-aborted signal stands down with no newer goto() arriving
|
|
299
|
+
* to correct it, leaving the nested outlet stuck — reachable, because a
|
|
300
|
+
* hash-only navigation aborts the outstanding one without producing a
|
|
301
|
+
* replacement. Supersession is the counter's job.
|
|
302
|
+
*/
|
|
303
|
+
private _routeChild(child: Routes, tail: string) {
|
|
304
|
+
if (!child.hasRouteFor(tail)) {
|
|
305
|
+
child._supersede();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
void child.goto(tail).catch((err) => {
|
|
309
|
+
queueMicrotask(() => {
|
|
310
|
+
throw err;
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
303
315
|
/**
|
|
304
316
|
* Invalidate any in-flight `goto()` on this controller without starting a
|
|
305
317
|
* new one. Same-class access, so `_gotoSeq` stays private to `Routes`.
|
|
@@ -335,30 +347,48 @@ export class Routes implements ReactiveController {
|
|
|
335
347
|
* server-rendered page, an export endpoint, or a GET form still works.
|
|
336
348
|
*/
|
|
337
349
|
hasRouteFor(pathname: string): boolean {
|
|
338
|
-
//
|
|
339
|
-
// behaves as if it had a single `/*` route.
|
|
340
|
-
|
|
350
|
+
// A fallback matches everything, and a controller with no routes of its own
|
|
351
|
+
// behaves as if it had a single `/*` route (goto()'s special case). Either
|
|
352
|
+
// way the answer is yes without running a single pattern — worth
|
|
353
|
+
// short-circuiting, since `Router` asks this on every navigation.
|
|
354
|
+
if (this.fallback !== undefined || this.routes.length === 0) {
|
|
341
355
|
return true;
|
|
342
356
|
}
|
|
343
|
-
|
|
357
|
+
// `test()`, not `_match()`: this only needs the yes/no, and `exec()` pays
|
|
358
|
+
// ~8x on a hit to build a groups object the caller would throw away.
|
|
359
|
+
return this.routes.some((r) => getPattern(r).test({pathname}));
|
|
344
360
|
}
|
|
345
361
|
|
|
346
362
|
/**
|
|
347
|
-
* Matches `
|
|
363
|
+
* Matches `pathname` against the installed routes and returns the first match
|
|
364
|
+
* with its parsed parameters, or the fallback's match if one is configured.
|
|
365
|
+
*
|
|
366
|
+
* One `exec()` per candidate rather than `test()` to select and `exec()` to
|
|
367
|
+
* extract: that ran the winning pattern twice, and every caller that wants a
|
|
368
|
+
* route wants its params too.
|
|
348
369
|
*/
|
|
349
|
-
private
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
|
|
370
|
+
private _match(pathname: string):
|
|
371
|
+
| {route: RouteConfig; params: {[key: string]: string | undefined}}
|
|
372
|
+
| undefined {
|
|
373
|
+
for (const route of this.routes) {
|
|
374
|
+
const result = getPattern(route).exec({pathname});
|
|
375
|
+
if (result !== null) {
|
|
376
|
+
return {route, params: result.pathname.groups};
|
|
377
|
+
}
|
|
355
378
|
}
|
|
356
|
-
if (this.fallback) {
|
|
357
|
-
|
|
358
|
-
// the public API but is added here to return a valid RouteConfig.
|
|
359
|
-
return {...this.fallback, path: '/*'};
|
|
379
|
+
if (this.fallback === undefined) {
|
|
380
|
+
return undefined;
|
|
360
381
|
}
|
|
361
|
-
|
|
382
|
+
// The fallback route behaves like it has a "/*" path. This is hidden from
|
|
383
|
+
// the public API but is added here to return a valid RouteConfig. The
|
|
384
|
+
// pattern is the shared one rather than one derived from this object:
|
|
385
|
+
// the spread produces a fresh object every call, which `patternCache` —
|
|
386
|
+
// keyed by identity — would miss, rebuilding a URLPattern per navigation.
|
|
387
|
+
const wildcard = getWildcardPattern();
|
|
388
|
+
return {
|
|
389
|
+
route: {...this.fallback, path: '/*'},
|
|
390
|
+
params: wildcard.exec({pathname})?.pathname.groups ?? {},
|
|
391
|
+
};
|
|
362
392
|
}
|
|
363
393
|
|
|
364
394
|
hostConnected() {
|
|
@@ -402,31 +432,17 @@ export class Routes implements ReactiveController {
|
|
|
402
432
|
|
|
403
433
|
e.stopImmediatePropagation();
|
|
404
434
|
e.onDisconnect = () => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
1
|
|
410
|
-
);
|
|
435
|
+
const index = this._childRoutes.indexOf(childRoutes);
|
|
436
|
+
if (index !== -1) {
|
|
437
|
+
this._childRoutes.splice(index, 1);
|
|
438
|
+
}
|
|
411
439
|
};
|
|
412
440
|
|
|
441
|
+
// A child that mounts under an existing tail match has to be caught up to
|
|
442
|
+
// it — it missed the propagation loop in goto() that ran before it existed.
|
|
413
443
|
const tailGroup = getTailGroup(this._currentParams);
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
// to `/x/unknown`), not an error. Without this the two call sites disagree
|
|
417
|
-
// — silent there, uncaught global throw here — for identical input.
|
|
418
|
-
if (tailGroup !== undefined && childRoutes.hasRouteFor(tailGroup)) {
|
|
419
|
-
// No signal here on purpose. The parent commits its own state before
|
|
420
|
-
// children run, so by the time a late child mounts the navigation may
|
|
421
|
-
// already have been aborted — handing it that signal makes it stand down
|
|
422
|
-
// with no newer goto() ever arriving to correct it, leaving the nested
|
|
423
|
-
// outlet blank permanently. The goto counter covers what matters
|
|
424
|
-
// (supersession by a newer goto).
|
|
425
|
-
void childRoutes.goto(tailGroup).catch((err) => {
|
|
426
|
-
queueMicrotask(() => {
|
|
427
|
-
throw err;
|
|
428
|
-
});
|
|
429
|
-
});
|
|
444
|
+
if (tailGroup !== undefined) {
|
|
445
|
+
this._routeChild(childRoutes, tailGroup);
|
|
430
446
|
}
|
|
431
447
|
};
|
|
432
448
|
}
|
|
@@ -436,13 +452,29 @@ export class Routes implements ReactiveController {
|
|
|
436
452
|
* wildcard at the end of a pathname pattern, like `/foo/*`
|
|
437
453
|
*/
|
|
438
454
|
const getTailGroup = (groups: {[key: string]: string | undefined}) => {
|
|
439
|
-
let
|
|
455
|
+
let tailIndex = -1;
|
|
440
456
|
for (const key of Object.keys(groups)) {
|
|
441
|
-
|
|
442
|
-
|
|
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;
|
|
443
475
|
}
|
|
444
476
|
}
|
|
445
|
-
return
|
|
477
|
+
return tailIndex < 0 ? undefined : groups[String(tailIndex)];
|
|
446
478
|
};
|
|
447
479
|
|
|
448
480
|
/**
|