lit-navigation-router 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/NOTICE.md +168 -0
- package/README.md +120 -0
- package/development/index.d.ts +9 -0
- package/development/index.d.ts.map +1 -0
- package/development/index.js +8 -0
- package/development/index.js.map +1 -0
- package/development/router.d.ts +81 -0
- package/development/router.d.ts.map +1 -0
- package/development/router.js +154 -0
- package/development/router.js.map +1 -0
- package/development/routes.d.ts +163 -0
- package/development/routes.d.ts.map +1 -0
- package/development/routes.js +363 -0
- package/development/routes.js.map +1 -0
- package/package.json +72 -0
- package/src/index.ts +8 -0
- package/src/router.ts +205 -0
- package/src/routes.ts +471 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
*
|
|
6
|
+
* Modifications Copyright 2026 VanLandingham Labs, same license. See NOTICE.md.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
9
|
+
export interface BaseRouteConfig {
|
|
10
|
+
name?: string | undefined;
|
|
11
|
+
render?: (params: {
|
|
12
|
+
[key: string]: string | undefined;
|
|
13
|
+
}) => unknown;
|
|
14
|
+
enter?: (params: {
|
|
15
|
+
[key: string]: string | undefined;
|
|
16
|
+
}) => Promise<boolean> | boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A RouteConfig that matches against a `path` string. `path` must be a
|
|
20
|
+
* [`URLPattern` compatible pathname pattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/pathname).
|
|
21
|
+
*/
|
|
22
|
+
export interface PathRouteConfig extends BaseRouteConfig {
|
|
23
|
+
path: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A RouteConfig that matches against a given [`URLPattern`](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern)
|
|
27
|
+
*
|
|
28
|
+
* While `URLPattern` can match against protocols, hostnames, and ports,
|
|
29
|
+
* routes will only be checked for matches if they're part of the current
|
|
30
|
+
* origin. This means that the pattern is limited to checking `pathname` and
|
|
31
|
+
* `search`.
|
|
32
|
+
*/
|
|
33
|
+
export interface URLPatternRouteConfig extends BaseRouteConfig {
|
|
34
|
+
pattern: URLPatternLike;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The part of `URLPattern` this router uses.
|
|
38
|
+
*
|
|
39
|
+
* Declared structurally rather than referencing the global so the emitted
|
|
40
|
+
* `.d.ts` is self-contained: the `/// <reference types="urlpattern-polyfill" />`
|
|
41
|
+
* above is not carried into declaration output, and `URLPattern` is not in
|
|
42
|
+
* TypeScript's bundled `lib.dom`, so a published package typed against the
|
|
43
|
+
* global fails a consumer build with `TS2304: Cannot find name 'URLPattern'`.
|
|
44
|
+
* A real `URLPattern` satisfies this, so passing one still type-checks.
|
|
45
|
+
*/
|
|
46
|
+
export interface URLPatternLike {
|
|
47
|
+
test(input: {
|
|
48
|
+
pathname: string;
|
|
49
|
+
}): boolean;
|
|
50
|
+
exec(input: {
|
|
51
|
+
pathname: string;
|
|
52
|
+
}): {
|
|
53
|
+
pathname: {
|
|
54
|
+
groups: {
|
|
55
|
+
[key: string]: string | undefined;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
} | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A description of a route, which path or pattern to match against, and a
|
|
62
|
+
* render() callback used to render a match to the outlet.
|
|
63
|
+
*/
|
|
64
|
+
export type RouteConfig = PathRouteConfig | URLPatternRouteConfig;
|
|
65
|
+
/**
|
|
66
|
+
* A reactive controller that performs location-based routing using a
|
|
67
|
+
* configuration of URL patterns and associated render callbacks.
|
|
68
|
+
*/
|
|
69
|
+
export declare class Routes implements ReactiveController {
|
|
70
|
+
private readonly _host;
|
|
71
|
+
routes: Array<RouteConfig>;
|
|
72
|
+
/**
|
|
73
|
+
* A default fallback route which will always be matched if none of the
|
|
74
|
+
* {@link routes} match. Implicitly matches to the path "/*".
|
|
75
|
+
*/
|
|
76
|
+
fallback?: BaseRouteConfig;
|
|
77
|
+
private readonly _childRoutes;
|
|
78
|
+
private _parentRoutes;
|
|
79
|
+
/** Monotonic goto counter; see the last-goto-wins note in goto(). */
|
|
80
|
+
private _gotoSeq;
|
|
81
|
+
private _currentPathname;
|
|
82
|
+
private _currentRoute;
|
|
83
|
+
private _currentParams;
|
|
84
|
+
/**
|
|
85
|
+
* Callback to call when this controller is disconnected.
|
|
86
|
+
*
|
|
87
|
+
* It's critical to call this immediately in hostDisconnected so that this
|
|
88
|
+
* controller instance doesn't receive a tail match meant for another route.
|
|
89
|
+
*/
|
|
90
|
+
private _onDisconnect;
|
|
91
|
+
constructor(host: ReactiveControllerHost & HTMLElement, routes: Array<RouteConfig>, options?: {
|
|
92
|
+
fallback?: BaseRouteConfig;
|
|
93
|
+
});
|
|
94
|
+
/**
|
|
95
|
+
* Returns a URL string of the current route, including parent routes,
|
|
96
|
+
* optionally replacing the local path with `pathname`.
|
|
97
|
+
*/
|
|
98
|
+
link(pathname?: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Navigates this routes controller to `pathname`.
|
|
101
|
+
*
|
|
102
|
+
* This does not navigate parent routes, so it isn't (yet) a general page
|
|
103
|
+
* navigation API. It does navigate child routes if pathname matches a
|
|
104
|
+
* pattern with a tail wildcard pattern (`/*`).
|
|
105
|
+
*
|
|
106
|
+
* Pass `options.signal` to make the navigation abandonable. `enter()` is
|
|
107
|
+
* awaited, so a second `goto()` can start — and finish — while the first is
|
|
108
|
+
* still resolving its route; without a signal the slower one commits last
|
|
109
|
+
* and the outlet ends up on a route the URL has already left. `Router`
|
|
110
|
+
* threads `NavigateEvent.signal` through for exactly this reason.
|
|
111
|
+
*/
|
|
112
|
+
goto(pathname: string, options?: {
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
}): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* The result of calling the current route's render() callback.
|
|
117
|
+
*/
|
|
118
|
+
outlet(): unknown;
|
|
119
|
+
/**
|
|
120
|
+
* The current parsed route parameters.
|
|
121
|
+
*/
|
|
122
|
+
get params(): {
|
|
123
|
+
[key: string]: string | undefined;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Invalidate any in-flight `goto()` on this controller without starting a
|
|
127
|
+
* new one. Same-class access, so `_gotoSeq` stays private to `Routes`.
|
|
128
|
+
*/
|
|
129
|
+
private _supersede;
|
|
130
|
+
/**
|
|
131
|
+
* True when this controller can render `pathname` — i.e. a route matches, or
|
|
132
|
+
* a fallback is configured.
|
|
133
|
+
*
|
|
134
|
+
* `Router` gates interception on this: intercepting a path we cannot render
|
|
135
|
+
* commits the URL and then throws out of `goto()`, leaving the address bar
|
|
136
|
+
* moved and the outlet stale. Letting the browser handle it instead means a
|
|
137
|
+
* server-rendered page, an export endpoint, or a GET form still works.
|
|
138
|
+
*/
|
|
139
|
+
hasRouteFor(pathname: string): boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Matches `url` against the installed routes and returns the first match.
|
|
142
|
+
*/
|
|
143
|
+
private _getRoute;
|
|
144
|
+
hostConnected(): void;
|
|
145
|
+
hostDisconnected(): void;
|
|
146
|
+
private _onRoutesConnected;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* This event is fired from Routes controllers when their host is connected to
|
|
150
|
+
* announce the child route and potentially connect to a parent routes controller.
|
|
151
|
+
*/
|
|
152
|
+
export declare class RoutesConnectedEvent extends Event {
|
|
153
|
+
static readonly eventName = "lit-routes-connected";
|
|
154
|
+
readonly routes: Routes;
|
|
155
|
+
onDisconnect?: () => void;
|
|
156
|
+
constructor(routes: Routes);
|
|
157
|
+
}
|
|
158
|
+
declare global {
|
|
159
|
+
interface HTMLElementEventMap {
|
|
160
|
+
[RoutesConnectedEvent.eventName]: RoutesConnectedEvent;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2021 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
5
|
+
*
|
|
6
|
+
* Modifications Copyright 2026 VanLandingham Labs, same license. See NOTICE.md.
|
|
7
|
+
*/
|
|
8
|
+
// A cache of URLPatterns created for PathRouteConfig.
|
|
9
|
+
// Rather than converting all given RoutConfigs to URLPatternRouteConfig, this
|
|
10
|
+
// lets us make `routes` mutable so users can add new PathRouteConfigs
|
|
11
|
+
// dynamically.
|
|
12
|
+
const patternCache = new WeakMap();
|
|
13
|
+
const isPatternConfig = (route) => route.pattern !== undefined;
|
|
14
|
+
const getPattern = (route) => {
|
|
15
|
+
if (isPatternConfig(route)) {
|
|
16
|
+
return route.pattern;
|
|
17
|
+
}
|
|
18
|
+
let pattern = patternCache.get(route);
|
|
19
|
+
if (pattern === undefined) {
|
|
20
|
+
patternCache.set(route, (pattern = new URLPattern({ pathname: route.path })));
|
|
21
|
+
}
|
|
22
|
+
return pattern;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A reactive controller that performs location-based routing using a
|
|
26
|
+
* configuration of URL patterns and associated render callbacks.
|
|
27
|
+
*/
|
|
28
|
+
export class Routes {
|
|
29
|
+
_host;
|
|
30
|
+
/*
|
|
31
|
+
* The currently installed set of routes in precedence order.
|
|
32
|
+
*
|
|
33
|
+
* This array is mutable. To dynamically add a new route you can write:
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* this._routes.routes.push({
|
|
37
|
+
* path: '/foo',
|
|
38
|
+
* render: () => html`<p>Foo</p>`,
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* Mutating this property does not trigger any route transitions. If the
|
|
43
|
+
* changes may result is a different route matching for the current path, you
|
|
44
|
+
* must instigate a route update with `goto()`.
|
|
45
|
+
*/
|
|
46
|
+
routes = [];
|
|
47
|
+
/**
|
|
48
|
+
* A default fallback route which will always be matched if none of the
|
|
49
|
+
* {@link routes} match. Implicitly matches to the path "/*".
|
|
50
|
+
*/
|
|
51
|
+
fallback;
|
|
52
|
+
/*
|
|
53
|
+
* The current set of child Routes controllers. These are connected via
|
|
54
|
+
* the routes-connected event.
|
|
55
|
+
*/
|
|
56
|
+
_childRoutes = [];
|
|
57
|
+
_parentRoutes;
|
|
58
|
+
/*
|
|
59
|
+
* State related to the current matching route.
|
|
60
|
+
*
|
|
61
|
+
* We keep this so that consuming code can access current parameters, and so
|
|
62
|
+
* that we can propagate tail matches to child routes if they are added after
|
|
63
|
+
* navigation / matching.
|
|
64
|
+
*/
|
|
65
|
+
/** Monotonic goto counter; see the last-goto-wins note in goto(). */
|
|
66
|
+
_gotoSeq = 0;
|
|
67
|
+
_currentPathname;
|
|
68
|
+
_currentRoute;
|
|
69
|
+
_currentParams = {};
|
|
70
|
+
/**
|
|
71
|
+
* Callback to call when this controller is disconnected.
|
|
72
|
+
*
|
|
73
|
+
* It's critical to call this immediately in hostDisconnected so that this
|
|
74
|
+
* controller instance doesn't receive a tail match meant for another route.
|
|
75
|
+
*/
|
|
76
|
+
// TODO (justinfagnani): Do we need this now that we have a direct reference
|
|
77
|
+
// to the parent? We can call `this._parentRoutes.disconnect(this)`.
|
|
78
|
+
_onDisconnect;
|
|
79
|
+
constructor(host, routes, options) {
|
|
80
|
+
(this._host = host).addController(this);
|
|
81
|
+
this.routes = [...routes];
|
|
82
|
+
this.fallback = options?.fallback;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns a URL string of the current route, including parent routes,
|
|
86
|
+
* optionally replacing the local path with `pathname`.
|
|
87
|
+
*/
|
|
88
|
+
link(pathname) {
|
|
89
|
+
if (pathname?.startsWith('/')) {
|
|
90
|
+
return pathname;
|
|
91
|
+
}
|
|
92
|
+
if (pathname?.startsWith('.')) {
|
|
93
|
+
throw new Error('Not implemented');
|
|
94
|
+
}
|
|
95
|
+
pathname ??= this._currentPathname;
|
|
96
|
+
return (this._parentRoutes?.link() ?? '') + pathname;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Navigates this routes controller to `pathname`.
|
|
100
|
+
*
|
|
101
|
+
* This does not navigate parent routes, so it isn't (yet) a general page
|
|
102
|
+
* navigation API. It does navigate child routes if pathname matches a
|
|
103
|
+
* pattern with a tail wildcard pattern (`/*`).
|
|
104
|
+
*
|
|
105
|
+
* Pass `options.signal` to make the navigation abandonable. `enter()` is
|
|
106
|
+
* awaited, so a second `goto()` can start — and finish — while the first is
|
|
107
|
+
* still resolving its route; without a signal the slower one commits last
|
|
108
|
+
* and the outlet ends up on a route the URL has already left. `Router`
|
|
109
|
+
* threads `NavigateEvent.signal` through for exactly this reason.
|
|
110
|
+
*/
|
|
111
|
+
async goto(pathname, options) {
|
|
112
|
+
// TODO (justinfagnani): handle absolute vs relative paths separately.
|
|
113
|
+
// TODO (justinfagnani): generalize this to handle query params and
|
|
114
|
+
// fragments. It currently only handles path names because it's easier to
|
|
115
|
+
// completely disregard the origin for now. The click handler only does
|
|
116
|
+
// an in-page navigation if the origin matches anyway.
|
|
117
|
+
const signal = options?.signal;
|
|
118
|
+
// Last-goto-wins, per controller. The navigation signal alone is not
|
|
119
|
+
// enough: a child controller mounts as a *result* of its parent's render,
|
|
120
|
+
// so its first goto() comes from `_onRoutesConnected` — after the parent's
|
|
121
|
+
// navigation has already finished, and therefore with a signal that will
|
|
122
|
+
// never abort. Without this counter a slow first child load commits over a
|
|
123
|
+
// newer one. This also keeps `Routes` correct when used on its own, with
|
|
124
|
+
// no `Router` and no Navigation API in the picture.
|
|
125
|
+
const seq = ++this._gotoSeq;
|
|
126
|
+
const superseded = () => signal?.aborted === true || seq !== this._gotoSeq;
|
|
127
|
+
let tailGroup;
|
|
128
|
+
if (this.routes.length === 0 && this.fallback === undefined) {
|
|
129
|
+
// If a routes controller has none of its own routes it acts like it has
|
|
130
|
+
// one route of `/*` so that it passes the whole pathname as a tail
|
|
131
|
+
// match.
|
|
132
|
+
tailGroup = pathname;
|
|
133
|
+
this._currentPathname = '';
|
|
134
|
+
// Simulate a tail group with the whole pathname
|
|
135
|
+
this._currentParams = { 0: tailGroup };
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const route = this._getRoute(pathname);
|
|
139
|
+
if (route === undefined) {
|
|
140
|
+
throw new Error(`No route found for ${pathname}`);
|
|
141
|
+
}
|
|
142
|
+
const pattern = getPattern(route);
|
|
143
|
+
const result = pattern.exec({ pathname });
|
|
144
|
+
const params = result?.pathname.groups ?? {};
|
|
145
|
+
tailGroup = getTailGroup(params);
|
|
146
|
+
if (typeof route.enter === 'function') {
|
|
147
|
+
const success = await route.enter(params);
|
|
148
|
+
// If enter() returns false, cancel this navigation
|
|
149
|
+
if (success === false) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// A newer navigation superseded this one while `enter` was awaiting.
|
|
154
|
+
// Committing now would swap the outlet onto a route the URL has left.
|
|
155
|
+
if (superseded()) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Only update route state if the enter handler completes successfully
|
|
159
|
+
this._currentRoute = route;
|
|
160
|
+
this._currentParams = params;
|
|
161
|
+
this._currentPathname =
|
|
162
|
+
tailGroup === undefined
|
|
163
|
+
? pathname
|
|
164
|
+
: pathname.substring(0, pathname.length - tailGroup.length);
|
|
165
|
+
}
|
|
166
|
+
// Propagate the tail match to children — deliberately NOT awaited.
|
|
167
|
+
//
|
|
168
|
+
// Awaiting looks like it would make `navigation.finished` cover the whole
|
|
169
|
+
// tree, and an earlier revision of this fork did it. It is wrong twice
|
|
170
|
+
// over. At this point `requestUpdate()` has not run, so `_childRoutes`
|
|
171
|
+
// still holds the *outgoing* branch's controller: awaiting it gates the
|
|
172
|
+
// parent's outlet swap on an `enter()` for a tail that controller will
|
|
173
|
+
// never render (a hung one blocks the navigation forever), and if that
|
|
174
|
+
// child has no route for the new tail its `No route found` throw
|
|
175
|
+
// propagates out of here and `requestUpdate()` below never runs — URL
|
|
176
|
+
// committed, outlet stranded, i.e. this fork's own thesis bug one level
|
|
177
|
+
// down. Nested supersession is handled by the goto counter above, not by
|
|
178
|
+
// awaiting. Errors are swallowed rather than left as unhandled rejections.
|
|
179
|
+
if (tailGroup !== undefined) {
|
|
180
|
+
for (const childRoutes of this._childRoutes) {
|
|
181
|
+
// No signal, for the same reason as the late-mount path below: the
|
|
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
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
this._host.requestUpdate();
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The result of calling the current route's render() callback.
|
|
212
|
+
*/
|
|
213
|
+
outlet() {
|
|
214
|
+
return this._currentRoute?.render?.(this._currentParams);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The current parsed route parameters.
|
|
218
|
+
*/
|
|
219
|
+
get params() {
|
|
220
|
+
return this._currentParams;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Invalidate any in-flight `goto()` on this controller without starting a
|
|
224
|
+
* new one. Same-class access, so `_gotoSeq` stays private to `Routes`.
|
|
225
|
+
*/
|
|
226
|
+
_supersede(seen = new Set()) {
|
|
227
|
+
// Unreachable defence in depth. Upstream *can* produce a `_childRoutes`
|
|
228
|
+
// cycle — a host carrying two Routes controllers, disconnected and
|
|
229
|
+
// reconnected, ends up with each registered as the other's child — but
|
|
230
|
+
// `hostDisconnected` below removes the listener that causes it, and a test
|
|
231
|
+
// asserts the cycle cannot form. Kept because an unguarded recursive walk
|
|
232
|
+
// over a cycle is a stack overflow rather than a misrender.
|
|
233
|
+
if (seen.has(this)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
seen.add(this);
|
|
237
|
+
this._gotoSeq++;
|
|
238
|
+
// Recursive: on the navigating branch the child's own propagation loop
|
|
239
|
+
// reaches the grandchildren, but a skipped child never runs one — so
|
|
240
|
+
// without this an in-flight grandchild `enter()` stays current and commits
|
|
241
|
+
// over a URL that has moved on, the same defect one level deeper.
|
|
242
|
+
for (const child of this._childRoutes) {
|
|
243
|
+
child._supersede(seen);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* True when this controller can render `pathname` — i.e. a route matches, or
|
|
248
|
+
* a fallback is configured.
|
|
249
|
+
*
|
|
250
|
+
* `Router` gates interception on this: intercepting a path we cannot render
|
|
251
|
+
* commits the URL and then throws out of `goto()`, leaving the address bar
|
|
252
|
+
* moved and the outlet stale. Letting the browser handle it instead means a
|
|
253
|
+
* server-rendered page, an export endpoint, or a GET form still works.
|
|
254
|
+
*/
|
|
255
|
+
hasRouteFor(pathname) {
|
|
256
|
+
// Mirrors goto()'s special case: a controller with no routes of its own
|
|
257
|
+
// behaves as if it had a single `/*` route.
|
|
258
|
+
if (this.routes.length === 0 && this.fallback === undefined) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
return this._getRoute(pathname) !== undefined;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Matches `url` against the installed routes and returns the first match.
|
|
265
|
+
*/
|
|
266
|
+
_getRoute(pathname) {
|
|
267
|
+
const matchedRoute = this.routes.find((r) => getPattern(r).test({ pathname: pathname }));
|
|
268
|
+
if (matchedRoute || this.fallback === undefined) {
|
|
269
|
+
return matchedRoute;
|
|
270
|
+
}
|
|
271
|
+
if (this.fallback) {
|
|
272
|
+
// The fallback route behaves like it has a "/*" path. This is hidden from
|
|
273
|
+
// the public API but is added here to return a valid RouteConfig.
|
|
274
|
+
return { ...this.fallback, path: '/*' };
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
hostConnected() {
|
|
279
|
+
this._host.addEventListener(RoutesConnectedEvent.eventName, this._onRoutesConnected);
|
|
280
|
+
const event = new RoutesConnectedEvent(this);
|
|
281
|
+
this._host.dispatchEvent(event);
|
|
282
|
+
this._onDisconnect = event.onDisconnect;
|
|
283
|
+
}
|
|
284
|
+
hostDisconnected() {
|
|
285
|
+
// Remove the listener hostConnected added. Without this a host that is
|
|
286
|
+
// disconnected and reconnected (a repeat() reorder, a tab swap) leaves the
|
|
287
|
+
// sibling controller's listener installed, so on the second connect it
|
|
288
|
+
// claims the re-dispatching controller as *its* child and the pair point
|
|
289
|
+
// at each other — a real `_childRoutes` cycle, which recursive walks turn
|
|
290
|
+
// into a stack overflow.
|
|
291
|
+
this._host.removeEventListener(RoutesConnectedEvent.eventName, this._onRoutesConnected);
|
|
292
|
+
// When this child routes controller is disconnected because a parent
|
|
293
|
+
// outlet rendered a different template, disconnecting will ensure that
|
|
294
|
+
// this controller doesn't receive a tail match meant for another route.
|
|
295
|
+
this._onDisconnect?.();
|
|
296
|
+
this._parentRoutes = undefined;
|
|
297
|
+
}
|
|
298
|
+
_onRoutesConnected = (e) => {
|
|
299
|
+
// Don't handle the event fired by this routes controller, which we get
|
|
300
|
+
// because we do this.dispatchEvent(...)
|
|
301
|
+
if (e.routes === this) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const childRoutes = e.routes;
|
|
305
|
+
this._childRoutes.push(childRoutes);
|
|
306
|
+
childRoutes._parentRoutes = this;
|
|
307
|
+
e.stopImmediatePropagation();
|
|
308
|
+
e.onDisconnect = () => {
|
|
309
|
+
// Remove route from this._childRoutes:
|
|
310
|
+
// `>>> 0` converts -1 to 2**32-1
|
|
311
|
+
this._childRoutes?.splice(this._childRoutes.indexOf(childRoutes) >>> 0, 1);
|
|
312
|
+
};
|
|
313
|
+
const tailGroup = getTailGroup(this._currentParams);
|
|
314
|
+
// Same structural filter as the propagation path in goto(): a child that
|
|
315
|
+
// mounts under a tail it cannot render is the expected case (a deep link
|
|
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
|
+
});
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Returns the tail of a pathname groups object. This is the match from a
|
|
335
|
+
* wildcard at the end of a pathname pattern, like `/foo/*`
|
|
336
|
+
*/
|
|
337
|
+
const getTailGroup = (groups) => {
|
|
338
|
+
let tailKey;
|
|
339
|
+
for (const key of Object.keys(groups)) {
|
|
340
|
+
if (/\d+/.test(key) && (tailKey === undefined || key > tailKey)) {
|
|
341
|
+
tailKey = key;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return tailKey && groups[tailKey];
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* This event is fired from Routes controllers when their host is connected to
|
|
348
|
+
* announce the child route and potentially connect to a parent routes controller.
|
|
349
|
+
*/
|
|
350
|
+
export class RoutesConnectedEvent extends Event {
|
|
351
|
+
static eventName = 'lit-routes-connected';
|
|
352
|
+
routes;
|
|
353
|
+
onDisconnect;
|
|
354
|
+
constructor(routes) {
|
|
355
|
+
super(RoutesConnectedEvent.eventName, {
|
|
356
|
+
bubbles: true,
|
|
357
|
+
composed: true,
|
|
358
|
+
cancelable: false,
|
|
359
|
+
});
|
|
360
|
+
this.routes = routes;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lit-navigation-router",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "A router for Lit built on the Navigation API. Fork of @lit-labs/router.",
|
|
5
|
+
"license": "BSD-3-Clause",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/VanLandinghamLabs/lit-router.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"main": "development/index.js",
|
|
12
|
+
"module": "development/index.js",
|
|
13
|
+
"typings": "development/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./development/index.d.ts",
|
|
17
|
+
"default": "./development/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./router.js": {
|
|
20
|
+
"types": "./development/router.d.ts",
|
|
21
|
+
"default": "./development/router.js"
|
|
22
|
+
},
|
|
23
|
+
"./routes.js": {
|
|
24
|
+
"types": "./development/routes.d.ts",
|
|
25
|
+
"default": "./development/routes.js"
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"/development/",
|
|
31
|
+
"!/development/test/",
|
|
32
|
+
"/src/",
|
|
33
|
+
"!/src/test/",
|
|
34
|
+
"/NOTICE.md"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepare": "npm run build",
|
|
38
|
+
"build": "tsc --build --pretty && npm run build:assets",
|
|
39
|
+
"clean": "rm -rf development tsconfig.tsbuildinfo",
|
|
40
|
+
"check-types": "tsc --noEmit -p tsconfig.json",
|
|
41
|
+
"test": "npm run build && web-test-runner --config web-test-runner.config.js",
|
|
42
|
+
"test:watch": "npm run build && web-test-runner --config web-test-runner.config.js --watch",
|
|
43
|
+
"build:assets": "mkdir -p development/test && cp src/test/*.html development/test/",
|
|
44
|
+
"prepublishOnly": "npm run clean && npm test"
|
|
45
|
+
},
|
|
46
|
+
"author": "VanLandingham Labs",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"lit": "^3.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@open-wc/testing": "^4.0.0",
|
|
52
|
+
"@types/mocha": "^10.0.10",
|
|
53
|
+
"@web/dev-server-esbuild": "^1.0.4",
|
|
54
|
+
"@web/test-runner": "^0.20.0",
|
|
55
|
+
"@web/test-runner-playwright": "^0.11.1",
|
|
56
|
+
"typescript": "^5.7.0",
|
|
57
|
+
"urlpattern-polyfill": "^10.1.0"
|
|
58
|
+
},
|
|
59
|
+
"keywords": [
|
|
60
|
+
"lit",
|
|
61
|
+
"router",
|
|
62
|
+
"navigation-api",
|
|
63
|
+
"spa",
|
|
64
|
+
"web-components",
|
|
65
|
+
"lit-element"
|
|
66
|
+
],
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/VanLandinghamLabs/lit-router/issues"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://github.com/VanLandinghamLabs/lit-router#readme",
|
|
71
|
+
"sideEffects": false
|
|
72
|
+
}
|