@firsthandjs/router 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Firsthand contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # @firsthandjs/router
2
+
3
+ Nested routes, real links, and route code that arrives when the route does.
4
+
5
+ **Documentation:** [guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/08-routing.md) · [API reference](https://github.com/firsthandjs/firsthand/blob/main/docs/reference/router.md) · [all docs](https://github.com/firsthandjs/firsthand/blob/main/docs/README.md)
6
+
7
+ ```
8
+ npm install @firsthandjs/router
9
+ ```
10
+
11
+ 3.36 kB gzip. No dependencies other than `@firsthandjs/core` and `@firsthandjs/dom`.
12
+
13
+ ## The shape of it
14
+
15
+ ```tsx
16
+ import { render, component } from '@firsthandjs/dom';
17
+ import { Router, Outlet, NavLink, route, type RouteProps } from '@firsthandjs/router';
18
+
19
+ const Shell = component(() => (
20
+ <main>
21
+ <nav>
22
+ <NavLink to="/" end>
23
+ Home
24
+ </NavLink>
25
+ <NavLink to="/users">Users</NavLink>
26
+ <NavLink to="/reports" preload>
27
+ Reports
28
+ </NavLink>
29
+ </nav>
30
+ <Outlet />
31
+ </main>
32
+ ));
33
+
34
+ // `props.params` is typed by the route's path, below. Nothing says `id` twice.
35
+ const User = component<RouteProps<{ id: string }>>((props) => <h2>User {props.params.id}</h2>);
36
+
37
+ const routes = [
38
+ route({
39
+ path: '/',
40
+ component: Shell,
41
+ children: (child) => [
42
+ child({ index: true, component: Home }),
43
+ child({ path: 'users/:id', component: User }),
44
+ // Not in the initial bundle. Fetched when the route is entered — or on
45
+ // hover, because the link above asks for it.
46
+ child({ path: 'reports', lazy: () => import('./reports.js') }),
47
+ child({ path: '*', component: NotFound }),
48
+ ],
49
+ }),
50
+ ];
51
+
52
+ render(() => <Router routes={routes} />);
53
+ ```
54
+
55
+ ## How it differs from react-router
56
+
57
+ The path syntax, the ranking rules, nesting, `Outlet`, `Link`, `NavLink`,
58
+ `Navigate`, `useNavigate`, `useLocation`, `useRouteParams` and `useSearchParams` all
59
+ behave as they do there. Two things are different on purpose.
60
+
61
+ **Routes are objects, not elements.** React Router can write
62
+ `<Route path="/users" element={<Users />} />` because a React element is a
63
+ description that the router reads before anything renders. A Firsthand element is
64
+ DOM — `<Users />` builds it — so there would be nothing to read. Writing the
65
+ route table as data is the same information without the costume, and it is also
66
+ what makes `lazy` straightforward: a route can name a module it has not loaded,
67
+ which an element cannot.
68
+
69
+ **Read-only hooks return cells.** A component body runs once, so a hook that
70
+ returned a value would return the value it had at setup and never change.
71
+
72
+ ```tsx
73
+ const params = useRouteParams<{ id: string }>();
74
+ return <h2>User {params.value.id}</h2>; // fine-grained: updates this text node
75
+ ```
76
+
77
+ Navigating from `/users/1` to `/users/2` does not re-create the page. The route
78
+ is the same, so the component instance is the same; only the text that read the
79
+ parameter is rewritten. Changing to a _different_ route disposes the old page
80
+ and builds the new one.
81
+
82
+ ## Code splitting
83
+
84
+ ```ts
85
+ { path: 'reports', lazy: () => import('./reports.js') }
86
+ ```
87
+
88
+ `lazy` is called the first time the route is entered, once per route, and the
89
+ result is cached. Until it resolves the router renders `pending` — the route's
90
+ own, or the one passed to `Router` — and swaps to the real page by itself when
91
+ the module arrives, because reading the loaded component is an ordinary
92
+ reactive read.
93
+
94
+ `<Link preload>` starts that import on hover and on focus, so the usual case is
95
+ that the chunk is already there when the click happens. `preloadRoutes(routes,
96
+ path)` does the same from anywhere.
97
+
98
+ The module may export the component as `default` or be the component itself.
99
+
100
+ ## API
101
+
102
+ | Export | What it is |
103
+ | --------------------------------------------------------------------------- | --------------------------------------------------------------------- |
104
+ | `Router` | Root. Takes `routes`, and optionally `history`, `basename`, `pending` |
105
+ | `Outlet` | Where a route renders its matched child |
106
+ | `Link`, `NavLink` | A real `<a href>`; `NavLink` adds a class while active |
107
+ | `Navigate` | Redirects when rendered |
108
+ | `useRouter` | The router itself: its history, routes and matches |
109
+ | `useLocation`, `useRouteParams`, `useMatches`, `useMatch` | Cells |
110
+ | `useSearchParams` | `[cell, setter]` |
111
+ | `useNavigate` | `(to: string \| number, options?) => void` |
112
+ | `useBasePath`, `resolvePath`, `isActivePath` | The rules links use, exposed |
113
+ | `createBrowserHistory`, `createHashHistory`, `createMemoryHistory` | History adapters |
114
+ | `route` | One route, with its parameters read from its own path |
115
+ | `matchRoutes`, `routeComponent`, `preloadRoutes` | The matcher, usable on its own |
116
+ | `compilePattern`, `matchPattern`, `normalizePath`, `sameParams`, `segments` | Path utilities |
117
+
118
+ ### Paths
119
+
120
+ - `/users/:id` — a parameter, available as `params.value.id`, URL-decoded
121
+ - `/users/:id?` — an optional segment
122
+ - `/files/*` — a splat, available as `params.value['*']`
123
+ - `index: true` — matches when the parent matches and nothing is left over
124
+
125
+ Routes are **ranked**, not tried in order: `/users/new` wins over `/users/:id`
126
+ however the array is sorted.
127
+
128
+ ### Links
129
+
130
+ `Link` intercepts only a plain left-click on a same-window link. Modified
131
+ clicks, middle clicks and `target="_blank"` are left to the browser, and the
132
+ `href` is always a real URL — the status bar, "open in new tab" and crawlers all
133
+ work. Any prop the router does not use is forwarded to the anchor.
134
+
135
+ ### History
136
+
137
+ `createBrowserHistory(basename)` is the default. `createHashHistory()` suits a
138
+ host that cannot rewrite every path to one document. `createMemoryHistory()` is
139
+ what tests should use:
140
+
141
+ ```tsx
142
+ const history = createMemoryHistory(['/users/1']);
143
+ const view = mount(() => <Router routes={routes} history={history} />);
144
+ history.push('/users/2'); // synchronous: the DOM is already updated
145
+ ```
146
+
147
+ A history the `Router` created is disposed with it; one you passed in is yours.
148
+
149
+ ## Deployment
150
+
151
+ A router that owns paths the file system does not know about needs its host to
152
+ serve the application's document for unknown paths. That is one rule in most
153
+ static hosts, and `createHashHistory()` is the way out when it is not available.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * History adapters.
3
+ *
4
+ * Three, with the same interface: the browser's own history, a hash-based one
5
+ * for static hosts, and an in-memory one for tests and for rendering outside a
6
+ * browser. The location is a signal, so everything downstream — matching, the
7
+ * active class on a link, a component reading a parameter — is an ordinary
8
+ * reactive read rather than a subscription the application has to manage.
9
+ */
10
+ import { type ReadonlyCell } from '@firsthandjs/core';
11
+ export interface Location {
12
+ readonly pathname: string;
13
+ /** Including the leading `?`, or empty. */
14
+ readonly search: string;
15
+ /** Including the leading `#`, or empty. */
16
+ readonly hash: string;
17
+ /** Whatever was passed to `push`/`replace`. */
18
+ readonly state: unknown;
19
+ /** Changes on every navigation, including to the same URL. */
20
+ readonly key: string;
21
+ }
22
+ export interface NavigateOptions {
23
+ readonly replace?: boolean;
24
+ readonly state?: unknown;
25
+ }
26
+ export interface History {
27
+ readonly location: ReadonlyCell<Location>;
28
+ push(to: string, options?: NavigateOptions): void;
29
+ replace(to: string, options?: NavigateOptions): void;
30
+ go(delta: number): void;
31
+ /** Turns a router path into something an `href` can use. */
32
+ href(to: string): string;
33
+ /** Stops listening. Returns nothing; calling it twice is harmless. */
34
+ dispose(): void;
35
+ }
36
+ /** Splits a URL-ish string into its three parts. */
37
+ export declare function parsePath(to: string): Omit<Location, 'state' | 'key'>;
38
+ /**
39
+ * The browser's history: real URLs, real back button.
40
+ *
41
+ * `basename` lets an application live under a sub-path without every route
42
+ * knowing about it.
43
+ */
44
+ export declare function createBrowserHistory(basename?: string): History;
45
+ /** Hash routing, for hosts that cannot rewrite every path to one document. */
46
+ export declare function createHashHistory(): History;
47
+ /**
48
+ * An in-memory history.
49
+ *
50
+ * What tests should use, and what a server would use: no `window`, and the
51
+ * entry stack is inspectable.
52
+ */
53
+ export declare function createMemoryHistory(initial?: readonly string[]): History;
54
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAU,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC1C,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;IACrD,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,sEAAsE;IACtE,OAAO,IAAI,IAAI,CAAC;CACjB;AAKD,oDAAoD;AACpD,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,OAAO,GAAG,KAAK,CAAC,CAQrE;AAMD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,SAAK,GAAG,OAAO,CA2C3D;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,IAAI,OAAO,CAqC3C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,SAAS,MAAM,EAAU,GAAG,OAAO,CA+B/E"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Hooks.
3
+ *
4
+ * The names are react-router's, the return types are not: each read-only hook
5
+ * hands back a cell rather than a value, because a component body runs once.
6
+ * `useRouteParams().value.id` inside a JSX expression is a fine-grained read, and
7
+ * changing the parameter updates exactly that text node.
8
+ */
9
+ import { type ReadonlyCell } from '@firsthandjs/core';
10
+ import { type RouterState } from './router.js';
11
+ import { type Params } from './match.js';
12
+ import type { Location, NavigateOptions } from './history.js';
13
+ import type { RouteMatch } from './routes.js';
14
+ /** What `useNavigate` returns: a path, or a delta through the history stack. */
15
+ export type NavigateFunction = (to: string | number, options?: NavigateOptions) => void;
16
+ /**
17
+ * Resolves `to` against a base path.
18
+ *
19
+ * `..` steps up a path segment. React Router steps up a *route* level, which
20
+ * differs only when one route owns several segments; the path rule is the one
21
+ * that can be explained without knowing the route tree.
22
+ */
23
+ export declare function resolvePath(to: string, base: string): string;
24
+ /** The path everything relative in the surrounding route resolves against. */
25
+ export declare function useBasePath(): ReadonlyCell<string>;
26
+ /**
27
+ * The router above this component.
28
+ *
29
+ * The same shape as `useQueryClient()` in `@firsthandjs/query`: a `use*` hook that
30
+ * returns the thing itself, because a router does not change under you.
31
+ */
32
+ export declare function useRouter(): RouterState;
33
+ export declare function useLocation(): ReadonlyCell<Location>;
34
+ /**
35
+ * The parameters of the deepest matched route, merged down the chain.
36
+ *
37
+ * The type parameter names what the route captures — `useRouteParams<{ id: string
38
+ * }>()` — which is what lets `params.value.id` be written as a property
39
+ * instead of an index. It is an assertion about the route, exactly as it is in
40
+ * react-router, and it appears only in the return type.
41
+ */
42
+ export declare function useRouteParams<P extends Params = Params>(): ReadonlyCell<P>;
43
+ /** The whole matched chain, outermost first. */
44
+ export declare function useMatches(): ReadonlyCell<readonly RouteMatch[]>;
45
+ export declare function useNavigate(): NavigateFunction;
46
+ /** Whether a path is active, by the same rule `NavLink` uses. */
47
+ export declare function isActivePath(current: string, target: string, end: boolean): boolean;
48
+ export declare function useMatch<P extends Params = Params>(path: string): ReadonlyCell<{
49
+ params: P;
50
+ } | null>;
51
+ /** Reads and writes the query string. */
52
+ export declare function useSearchParams(): [
53
+ ReadonlyCell<URLSearchParams>,
54
+ (next: URLSearchParams | Record<string, string>, options?: NavigateOptions) => void
55
+ ];
56
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAML,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,gFAAgF;AAChF,MAAM,MAAM,gBAAgB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,IAAI,CAAC;AAExF;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAmB5D;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,IAAI,YAAY,CAAC,MAAM,CAAC,CAMlD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,IAAI,WAAW,CAEvC;AAED,wBAAgB,WAAW,IAAI,YAAY,CAAC,QAAQ,CAAC,CAEpD;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,KAAK,YAAY,CAAC,CAAC,CAAC,CAW3E;AAED,gDAAgD;AAChD,wBAAgB,UAAU,IAAI,YAAY,CAAC,SAAS,UAAU,EAAE,CAAC,CAEhE;AAED,wBAAgB,WAAW,IAAI,gBAAgB,CAgB9C;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAInF;AAGD,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAChD,IAAI,EAAE,MAAM,GACX,YAAY,CAAC;IAAE,MAAM,EAAE,CAAC,CAAA;CAAE,GAAG,IAAI,CAAC,CAIpC;AAED,yCAAyC;AACzC,wBAAgB,eAAe,IAAI;IACjC,YAAY,CAAC,eAAe,CAAC;IAC7B,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,KAAK,IAAI;CACpF,CASA"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `@firsthandjs/router` — nested routes, real links, and route code loaded on
3
+ * demand.
4
+ *
5
+ * The API is react-router's, with two deliberate differences:
6
+ *
7
+ * 1. Routes are plain objects, not `<Route>` elements. Firsthand has no element
8
+ * descriptors to inspect before rendering, so a JSX route tree would be a
9
+ * costume rather than a design.
10
+ * 2. Read-only hooks return cells, not values, because a component body runs
11
+ * once. `useRouteParams().value.id` is a fine-grained read; a navigation updates
12
+ * the text node that read it and nothing else.
13
+ *
14
+ * Everything else — path syntax, ranking, nesting, `Outlet`, `Link`,
15
+ * `NavLink`, `Navigate`, `useNavigate`, `useLocation`, `useSearchParams` —
16
+ * behaves as it does there.
17
+ */
18
+ export { Router, Outlet, RouterContext } from './router.js';
19
+ export type { RouterProps, RouterState } from './router.js';
20
+ export { Link, NavLink } from './links.js';
21
+ export type { LinkProps, NavLinkProps } from './links.js';
22
+ export { Navigate } from './navigate.js';
23
+ export type { NavigateProps } from './navigate.js';
24
+ export { useRouter, useLocation, useMatch, useMatches, useNavigate, useRouteParams, useSearchParams, useBasePath, resolvePath, isActivePath, } from './hooks.js';
25
+ export type { NavigateFunction } from './hooks.js';
26
+ export { matchRoutes, routeComponent, preloadRoutes, route } from './routes.js';
27
+ export type { RouteBuilder, RouteDefinition, RouteMatch, RouteComponent, RouteProps, LazyModule, } from './routes.js';
28
+ export { createBrowserHistory, createHashHistory, createMemoryHistory, parsePath, } from './history.js';
29
+ export type { History, Location, NavigateOptions } from './history.js';
30
+ export { compilePattern, matchPattern, normalizePath, sameParams, segments } from './match.js';
31
+ export type { Params, ParamsOf, Pattern } from './match.js';
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5D,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EACL,SAAS,EACT,WAAW,EACX,QAAQ,EACR,UAAU,EACV,WAAW,EACX,cAAc,EACd,eAAe,EACf,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAChF,YAAY,EACV,YAAY,EACZ,eAAe,EACf,UAAU,EACV,cAAc,EACd,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,SAAS,GACV,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC/F,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{computed as M,createContext as T,onCleanup as me,provide as $,useContext as U}from"@firsthandjs/core";import{component as H}from"@firsthandjs/dom";import{isComponent as fe}from"@firsthandjs/dom/internal";import{part as he}from"@firsthandjs/dom/internal";import{signal as O}from"@firsthandjs/core";var Z=0,m=()=>`k${String(++Z)}`;function f(e){let t=e.indexOf("#"),n=t===-1?"":e.slice(t),r=t===-1?e:e.slice(0,t),o=r.indexOf("?"),a=o===-1?"":r.slice(o),i=o===-1?r:r.slice(0,o);return{pathname:i===""?"/":i,search:a,hash:n}}function C(e,t){return{...f(e),state:t,key:m()}}function S(e=""){let t=e.endsWith("/")?e.slice(0,-1):e,n=u=>t!==""&&u.startsWith(t)?u.slice(t.length)||"/":u,r=()=>({pathname:n(window.location.pathname),search:window.location.search,hash:window.location.hash,state:window.history.state,key:m()}),o=O(r()),a=()=>{o.value=r()};window.addEventListener("popstate",a);let i=u=>`${t}${u.startsWith("/")?u:`/${u}`}`,s=(u,c,w)=>{let B=c?.state??null;window.history[w?"replaceState":"pushState"](B,"",i(u)),o.value=C(u,B)};return{location:o,push:(u,c)=>{s(u,c,c?.replace===!0)},replace:(u,c)=>{s(u,c,!0)},go:u=>{window.history.go(u)},href:i,dispose:()=>{window.removeEventListener("popstate",a)}}}function ee(){let e=()=>({...f(window.location.hash.slice(1)||"/"),state:null,key:m()}),t=O(e()),n=()=>{t.value=e()};window.addEventListener("hashchange",n);let r=o=>`#${o.startsWith("/")?o:`/${o}`}`;return{location:t,push:(o,a)=>{a?.replace===!0?window.location.replace(r(o)):window.location.hash=r(o).slice(1),t.value={...f(o),state:a?.state??null,key:m()}},replace:(o,a)=>{window.location.replace(r(o)),t.value={...f(o),state:a?.state??null,key:m()}},go:o=>{window.history.go(o)},href:r,dispose:()=>{window.removeEventListener("hashchange",n)}}}function te(e=["/"]){let t=e.map(o=>C(o,null)),n=t.length-1,r=O(t[n]);return{location:r,push:(o,a)=>{let i=C(o,a?.state??null);a?.replace===!0?t[n]=i:(t.length=n+1,t.push(i),n++),r.value=i},replace:(o,a)=>{let i=C(o,a?.state??null);t[n]=i,r.value=i},go:o=>{let a=Math.min(Math.max(n+o,0),t.length-1);n=a,r.value=t[a]},href:o=>o,dispose:()=>{}}}import{signal as ue}from"@firsthandjs/core";function h(e,t){let n=Object.keys(e);return n.length===Object.keys(t).length&&n.every(r=>e[r]===t[r])}var ne=10,oe=6,re=4,ae=1,ie=2;function se(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function d(e){return e.split("/").filter(t=>t!=="")}function y(e,t){let n=d(e),r=[],o=n.length===0?ie:0,a="";for(let s of n){if(s==="*"){r.push("*"),o+=ae,a+="(?:/(.*))?";continue}if(s.startsWith(":")){let u=s.endsWith("?");r.push(s.slice(1,u?-1:void 0)),o+=u?re:oe,a+=u?"(?:/([^/]+))?":"/([^/]+)";continue}o+=ne,a+=`/${se(s)}`}let i=new RegExp(`^${a}${t?"/?$":"(?=/|$)"}`,"i");return{source:e,regex:i,keys:r,score:o,end:t}}function g(e,t){let n=e.regex.exec(t);if(n===null)return null;let r={};for(let o=0;o<e.keys.length;o++){let a=n[o+1];a!==void 0&&(r[e.keys[o]]=decodeURIComponent(a))}return{params:r,consumed:n[0]}}function v(e){return`/${d(e).join("/")}`}var z=new WeakMap;function ce(e){return e.index===!0?"":e.path??""}function W(e,t,n){for(let r of e){let o=[...t,r];n.push(le(o));let a=r.children;a!==void 0&&W(a,o,n)}}function le(e){let t=e.map((n,r)=>({route:n,pattern:y(ce(n),r===e.length-1)}));return{levels:t,score:t.reduce((n,r)=>n+r.pattern.score,0)}}function de(e){let t=z.get(e);if(t!==void 0)return t;let n=[];return W(e,[],n),n.sort((r,o)=>o.score-r.score),z.set(e,n),n}function L(e,t){for(let n of de(e)){let r=[],o=t===""?"/":t,a="",i={},s=!1;for(let u of n.levels){let c=g(u.pattern,o);if(c===null){s=!0;break}i={...i,...c.params},a+=c.consumed,r.push({route:u.route,params:i,pathname:a===""?"/":a}),o=o.slice(c.consumed.length)}if(!s)return r}return null}var V=new WeakMap;function k(e){if(e.component!==void 0)return e.component;let t=V.get(e);return t===void 0&&(t=ue(void 0),V.set(e,t),pe(e,t)),t.value}async function pe(e,t){let n=await e.lazy();t.value=typeof n=="function"?n:n.default}function D(e,t){let n=L(e,t);if(n!==null)for(let r of n)r.route.component===void 0&&r.route.lazy!==void 0&&k(r.route)}var I=e=>{let{children:t,...n}=e;return typeof t!="function"?e:{...n,children:t(I)}};var l=T(),P=T(-1,"route depth"),ye=()=>null,ge=Object.freeze({}),K=new WeakMap;function ve(e,t){if(fe(e))return e;let n=K.get(e);return n===void 0&&(n=H(r=>e(r),void 0,`firsthand/router:route(${t})`,"Route"),K.set(e,n)),n}function _(e){let t=U(l),n=M(()=>t.value.matches.value[e]?.route),r=M(()=>t.value.matches.value[e]?.params??ge,{equals:h}),o=new Proxy({},{get:(i,s)=>typeof s=="string"?r.value[s]:void 0,has:(i,s)=>typeof s=="string"&&s in r.value,ownKeys:()=>Object.keys(r.value),getOwnPropertyDescriptor:(i,s)=>typeof s=="string"&&s in r.value?{configurable:!0,enumerable:!0,value:r.value[s]}:void 0}),a=Object.freeze({params:o});return he(()=>{let i=n.value;if(i===void 0)return null;$(P,e);let s=k(i);return s===void 0?(i.pending??t.value.pending??ye)():ve(s,i.path??"")(a)})}var Pe=H(e=>{let t=e.history===void 0,n=e.history??S(e.basename??"");t&&me(()=>{n.dispose()});let r=e.routes,o=M(()=>L(r,n.location.value.pathname)??[]);return $(l,{history:n,routes:r,matches:o,pending:e.pending}),$(P,-1),_(0)},void 0,"firsthand/router:Router","Router"),Re=H(()=>_(U(P).value+1),void 0,"firsthand/router:Outlet","Outlet");import{bind as A,computed as q,useContext as Y}from"@firsthandjs/core";import{component as X}from"@firsthandjs/dom";import{insert as ke,on as be,rest as Ne,setAttribute as G,setClass as Oe,spread as Se}from"@firsthandjs/dom/internal";import{computed as b,useContext as p}from"@firsthandjs/core";function R(e,t){let n=e.search(/[?#]/),r=n===-1?e:e.slice(0,n),o=n===-1?"":e.slice(n);if(r.startsWith("/"))return`${v(r)}${o}`;let a=d(t);for(let i of d(r))if(i!=="."){if(i===".."){a.pop();continue}a.push(i)}return`/${a.join("/")}${o}`}function x(){let e=p(l),t=p(P).value;return b(()=>e.value.matches.value[t]?.pathname??"/")}function F(){return p(l).value}function j(){return F().history.location}function xe(){let e=p(l);return b(()=>{let t=e.value.matches.value;return t[t.length-1]?.params??{}},{equals:h})}function we(){return p(l).value.matches}function N(){let e=p(l),t=x();return(n,r)=>{let{history:o}=e.value;if(typeof n=="number"){o.go(n);return}let a=R(n,t.value);r?.replace===!0?o.replace(a,r):o.push(a,r)}}function E(e,t,n){let r=v(e),o=v(t);return n?r===o:r===o||r.startsWith(o==="/"?"/":`${o}/`)}function Ce(e){let t=y(e,!0),n=j();return b(()=>g(t,n.value.pathname))}function Le(){let e=j(),t=N();return[b(()=>new URLSearchParams(e.value.search)),(o,a)=>{let i=new URLSearchParams(o).toString();t(`${e.value.pathname}${i===""?"":`?${i}`}`,a)}]}var J=["to","replace","state","preload","onClick","children"],De=[...J,"end","activeClass","class"];function Me(e,t){return e.button===0&&!e.metaKey&&!e.altKey&&!e.ctrlKey&&!e.shiftKey&&(t.target===""||t.target==="_self")}function Q(e,t){let n=Y(l),r=x(),o=document.createElement("a"),a=q(()=>R(e.to,r.value));A(()=>{G(o,"href",n.value.history.href(a.value))});let i=Ne(e,t);if(A(()=>{Se(o,i)}),ke(o,()=>e.children),be(o,"click",s=>{let u=s;if(e.onClick?.(u),u.defaultPrevented||!Me(u,o))return;u.preventDefault();let{history:c}=n.value,w={replace:e.replace===!0,state:e.state};e.replace===!0?c.replace(a.value,w):c.push(a.value,w)}),e.preload===!0){let s=()=>{D(n.value.routes,a.value)};o.addEventListener("pointerenter",s),o.addEventListener("focus",s)}return o}var $e=X(e=>Q(e,J),void 0,"firsthand/router:Link","Link"),He=X(e=>{let t=Q(e,De),n=Y(l),r=x(),o=q(()=>E(n.value.history.location.value.pathname,R(e.to,r.value),e.end===!0));return A(()=>{let a=o.value?e.activeClass??"active":"",i=e.class??"";Oe(t,`${i}${i!==""&&a!==""?" ":""}${a}`),G(t,"aria-current",o.value?"page":null)}),t},void 0,"firsthand/router:NavLink","NavLink");import{onCleanup as je}from"@firsthandjs/core";import{component as Ee}from"@firsthandjs/dom";var Ae=Ee(e=>{let t=N(),n=!0;return je(()=>{n=!1}),queueMicrotask(()=>{n&&t(e.to,{replace:e.replace??!0,state:e.state})}),null},void 0,"firsthand/router:Navigate","Navigate");export{$e as Link,He as NavLink,Ae as Navigate,Re as Outlet,Pe as Router,l as RouterContext,y as compilePattern,S as createBrowserHistory,ee as createHashHistory,te as createMemoryHistory,E as isActivePath,g as matchPattern,L as matchRoutes,v as normalizePath,f as parsePath,D as preloadRoutes,R as resolvePath,I as route,k as routeComponent,h as sameParams,d as segments,x as useBasePath,j as useLocation,Ce as useMatch,we as useMatches,N as useNavigate,xe as useRouteParams,F as useRouter,Le as useSearchParams};
@@ -0,0 +1,28 @@
1
+ import { type View } from '@firsthandjs/dom';
2
+ export interface LinkProps {
3
+ readonly to: string;
4
+ readonly replace?: boolean;
5
+ readonly state?: unknown;
6
+ /**
7
+ * Starts loading the target route's code on hover and on focus.
8
+ *
9
+ * The chunk is then usually already there when the click happens, which is
10
+ * what makes on-demand loading feel like no loading at all.
11
+ */
12
+ readonly preload?: boolean;
13
+ readonly onClick?: (event: MouseEvent) => void;
14
+ readonly children?: View;
15
+ /** Anything else is forwarded to the anchor. */
16
+ readonly [attribute: string]: unknown;
17
+ }
18
+ export interface NavLinkProps extends LinkProps {
19
+ /** Active only on an exact match, rather than on a prefix. */
20
+ readonly end?: boolean;
21
+ /** Class added while the link is active. Defaults to `active`. */
22
+ readonly activeClass?: string;
23
+ readonly class?: string;
24
+ }
25
+ export declare const Link: import("@firsthandjs/dom").Component<LinkProps>;
26
+ /** A `Link` that knows whether it points at where you already are. */
27
+ export declare const NavLink: import("@firsthandjs/dom").Component<NavLinkProps>;
28
+ //# sourceMappingURL=links.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"links.d.ts","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AAaA,OAAO,EAAa,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAMxD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC;IACzB,gDAAgD;IAChD,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;CACvC;AAED,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IACvB,kEAAkE;IAClE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AA8DD,eAAO,MAAM,IAAI,iDAKhB,CAAC;AAEF,sEAAsE;AACtE,eAAO,MAAM,OAAO,oDAuBnB,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Path matching and route ranking.
3
+ *
4
+ * The rules are react-router's, because a router that matches *almost* like the
5
+ * one people know is worse than one that matches differently on purpose:
6
+ *
7
+ * - a segment beginning with `:` is a parameter,
8
+ * - a trailing `*` is a splat, captured as the `*` parameter,
9
+ * - a parameter may end with `?` to make that segment optional,
10
+ * - routes are ranked, not tried in order, so the most specific match wins
11
+ * regardless of how the array happens to be sorted.
12
+ *
13
+ * Matching is done on a pre-compiled regular expression per pattern, built once
14
+ * when the route tree is first walked.
15
+ */
16
+ /** Captured parameters, including `*` for a splat. */
17
+ export type Params = Readonly<Record<string, string>>;
18
+ /**
19
+ * Same keys, same values.
20
+ *
21
+ * Every navigation builds a fresh params object, so identity would report a
22
+ * change whenever anything in the URL moved; this reports one only when the
23
+ * parameters did.
24
+ */
25
+ export declare function sameParams(a: Params, b: Params): boolean;
26
+ /** Flattens an intersection so editors show one object rather than `A & B`. */
27
+ export type Simplify<T> = {
28
+ [K in keyof T]: T[K];
29
+ } & {};
30
+ /** The parameters one path segment contributes. */
31
+ type SegmentParams<S extends string> = S extends `:${infer Name}?` ? {
32
+ readonly [K in Name]?: string;
33
+ } : S extends `:${infer Name}` ? {
34
+ readonly [K in Name]: string;
35
+ } : S extends `*` ? {
36
+ readonly '*': string;
37
+ } : {};
38
+ /**
39
+ * The parameters a path declares, read from the path itself.
40
+ *
41
+ * `ParamsOf<'users/:id/files/*'>` is `{ id: string; '*': string }`. Nothing
42
+ * has to be declared twice: the route table already says what the path is.
43
+ */
44
+ export type ParamsOf<Path extends string> = Path extends `${infer Head}/${infer Rest}` ? SegmentParams<Head> & ParamsOf<Rest> : SegmentParams<Path>;
45
+ export interface Pattern {
46
+ readonly source: string;
47
+ readonly regex: RegExp;
48
+ readonly keys: readonly string[];
49
+ /** Higher wins. Static segments beat dynamic ones, which beat a splat. */
50
+ readonly score: number;
51
+ /** Whether the pattern must consume the whole path. */
52
+ readonly end: boolean;
53
+ }
54
+ /** Splits a path into segments, ignoring empty ones from leading/double slashes. */
55
+ export declare function segments(path: string): string[];
56
+ /**
57
+ * Compiles a path pattern.
58
+ *
59
+ * `end` is false for a parent route in a nested tree: it has to match a prefix
60
+ * so that its children can match the rest.
61
+ */
62
+ export declare function compilePattern(path: string, end: boolean): Pattern;
63
+ /** Runs a compiled pattern against a pathname. */
64
+ export declare function matchPattern(pattern: Pattern, pathname: string): {
65
+ params: Params;
66
+ consumed: string;
67
+ } | null;
68
+ /** Normalises a pathname: always a leading slash, never a trailing one. */
69
+ export declare function normalizePath(pathname: string): string;
70
+ export {};
71
+ //# sourceMappingURL=match.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"match.d.ts","sourceRoot":"","sources":["../src/match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,sDAAsD;AACtD,MAAM,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEtD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAGxD;AAED,+EAA+E;AAC/E,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,EAAE,CAAC;AAExD,mDAAmD;AACnD,KAAK,aAAa,CAAC,CAAC,SAAS,MAAM,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,GAAG,GAC9D;IAAE,QAAQ,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,EAAE,MAAM;CAAE,GACjC,CAAC,SAAS,IAAI,MAAM,IAAI,EAAE,GACxB;IAAE,QAAQ,EAAE,CAAC,IAAI,IAAI,GAAG,MAAM;CAAE,GAChC,CAAC,SAAS,GAAG,GACX;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAGxB,EAAE,CAAC;AAEX;;;;;GAKG;AACH,MAAM,MAAM,QAAQ,CAAC,IAAI,SAAS,MAAM,IAAI,IAAI,SAAS,GAAG,MAAM,IAAI,IAAI,MAAM,IAAI,EAAE,GAClF,aAAa,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,GACpC,aAAa,CAAC,IAAI,CAAC,CAAC;AAExB,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;CACvB;AAYD,oFAAoF;AACpF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CA0BlE;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAC1B,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,MAAM,GACf;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAa7C;AAED,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGtD"}
@@ -0,0 +1,14 @@
1
+ import type { NavigateOptions } from './history.js';
2
+ export interface NavigateProps extends NavigateOptions {
3
+ readonly to: string;
4
+ }
5
+ /**
6
+ * Navigates as soon as it is rendered.
7
+ *
8
+ * The navigation is deferred to a microtask rather than performed during
9
+ * setup: setup runs inside the render that is producing this element, and
10
+ * changing the location from there would re-enter the render that is still in
11
+ * progress. Nothing is painted in between, so the redirect is still invisible.
12
+ */
13
+ export declare const Navigate: import("@firsthandjs/dom").Component<NavigateProps>;
14
+ //# sourceMappingURL=navigate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigate.d.ts","sourceRoot":"","sources":["../src/navigate.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,WAAW,aAAc,SAAQ,eAAe;IACpD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,qDAiBpB,CAAC"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The router itself: context, the matched chain, and `Outlet`.
3
+ *
4
+ * Rendering a chain of nested routes needs one property that is easy to lose:
5
+ * navigating from `/users/1` to `/users/2` must *update* the user page, not
6
+ * replace it, because replacing it would throw away its DOM and its state for
7
+ * a change that only moved a parameter. So each depth watches the route
8
+ * *object* at that depth, not the match: the component is created again only
9
+ * when the route actually differs, and parameters reach it as ordinary
10
+ * reactive reads.
11
+ */
12
+ import { type ReadonlyCell } from '@firsthandjs/core';
13
+ import { type Component, type View } from '@firsthandjs/dom';
14
+ import { type History } from './history.js';
15
+ import { type RouteDefinition, type RouteMatch } from './routes.js';
16
+ export interface RouterState {
17
+ readonly history: History;
18
+ readonly routes: readonly RouteDefinition[];
19
+ /** The chain from the outermost matched route to the leaf. Empty if none matched. */
20
+ readonly matches: ReadonlyCell<readonly RouteMatch[]>;
21
+ /** Shown while a lazy route is loading, unless the route has its own. */
22
+ readonly pending: (() => View) | undefined;
23
+ }
24
+ export declare const RouterContext: import("@firsthandjs/core").Context<RouterState>;
25
+ /** How deep in the matched chain the surrounding route sits. */
26
+ export declare const DepthContext: import("@firsthandjs/core").Context<number>;
27
+ export interface RouterProps {
28
+ readonly routes: readonly RouteDefinition[];
29
+ /** Defaults to a browser history over the real URL. */
30
+ readonly history?: History;
31
+ /** Sub-path the application is served under; ignored if `history` is given. */
32
+ readonly basename?: string;
33
+ /** Shown while a lazy route is loading. */
34
+ readonly pending?: () => View;
35
+ }
36
+ /**
37
+ * The root of a routed application.
38
+ *
39
+ * A history it created is disposed with it; one that was passed in is not,
40
+ * because the caller owns that.
41
+ */
42
+ export declare const Router: Component<RouterProps>;
43
+ /** Where a route renders its matched child route. */
44
+ export declare const Outlet: Component<Record<string, never>>;
45
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAa,KAAK,SAAS,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAGxE,OAAO,EAAwB,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,UAAU,EAEhB,MAAM,aAAa,CAAC;AAGrB,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;IAC5C,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,SAAS,UAAU,EAAE,CAAC,CAAC;IACtD,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CAC5C;AAED,eAAO,MAAM,aAAa,kDAA+B,CAAC;AAE1D,gEAAgE;AAChE,eAAO,MAAM,YAAY,6CAAmC,CAAC;AAiG7D,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;IAC5C,uDAAuD;IACvD,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,+EAA+E;IAC/E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B;AAED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,wBAsBlB,CAAC;AAEF,qDAAqD;AACrD,eAAO,MAAM,MAAM,kCAKlB,CAAC"}
@@ -0,0 +1,116 @@
1
+ import type { View } from '@firsthandjs/dom';
2
+ import { type Params, type ParamsOf, type Simplify } from './match.js';
3
+ /**
4
+ * What a route hands its component.
5
+ *
6
+ * One prop, because there is exactly one thing a route knows that its
7
+ * component does not: what the URL captured. Reading `props.params` is an
8
+ * ordinary reactive read, so a navigation from `/users/1` to `/users/2`
9
+ * updates what read it and remounts nothing.
10
+ */
11
+ export interface RouteProps<P extends Params = Params> {
12
+ readonly params: P;
13
+ }
14
+ /** A component that a route renders. */
15
+ export type RouteComponent<P extends Params = Params> = (props: RouteProps<P>) => View;
16
+ /** What a `lazy` import may resolve to: the component, or a module holding it. */
17
+ export type LazyModule<P extends Params = Params> = RouteComponent<P> | {
18
+ readonly default: RouteComponent<P>;
19
+ };
20
+ export interface RouteDefinition {
21
+ /** Relative to the parent, unless it starts with `/`. */
22
+ readonly path?: string;
23
+ /** Matches when the parent matches and nothing is left over. */
24
+ readonly index?: boolean;
25
+ readonly component?: RouteComponent;
26
+ /**
27
+ * Loads the component when the route is first entered.
28
+ *
29
+ * This is the code-splitting seam: `lazy: () => import('./Settings.js')`
30
+ * keeps that module out of the initial bundle, and the bundler turns it into
31
+ * its own chunk. The import runs once per route and the result is cached.
32
+ */
33
+ readonly lazy?: () => Promise<LazyModule>;
34
+ /** Shown while `lazy` is loading, instead of the router's own fallback. */
35
+ readonly pending?: () => View;
36
+ readonly children?: readonly RouteDefinition[];
37
+ }
38
+ export interface RouteMatch {
39
+ readonly route: RouteDefinition;
40
+ readonly params: Params;
41
+ /** The portion of the pathname this route and its ancestors consumed. */
42
+ readonly pathname: string;
43
+ }
44
+ /**
45
+ * Matches a pathname against a route tree.
46
+ *
47
+ * Returns the chain from the outermost route to the matched leaf, which is
48
+ * what `Outlet` walks, or `null` when nothing matched.
49
+ */
50
+ export declare function matchRoutes(routes: readonly RouteDefinition[], pathname: string): RouteMatch[] | null;
51
+ /**
52
+ * The component for a route, loading it on first use.
53
+ *
54
+ * Reading this inside a reactive scope subscribes to the load, so the view
55
+ * swaps from the pending state to the real one by itself when the chunk
56
+ * arrives — no callback, no state machine in the application.
57
+ */
58
+ export declare function routeComponent(route: RouteDefinition): RouteComponent | undefined;
59
+ /**
60
+ * Loads the code for whatever `pathname` would render, without navigating.
61
+ *
62
+ * Called on hover or focus by `<Link preload>`, so the chunk is usually already
63
+ * there by the time the click happens.
64
+ */
65
+ export declare function preloadRoutes(routes: readonly RouteDefinition[], pathname: string): void;
66
+ /**
67
+ * One route, with its parameters read from its own path.
68
+ *
69
+ * The route table already says what each path is, so nothing is declared
70
+ * twice: `route({ path: 'users/:id', … })` types its component's
71
+ * `props.params` as `{ id: string }`, and a component that reaches for a
72
+ * parameter the path does not capture does not compile.
73
+ *
74
+ * ```tsx
75
+ * const routes = [
76
+ * route({
77
+ * path: '/',
78
+ * component: Shell,
79
+ * children: (child) => [
80
+ * child({ index: true, component: Home }),
81
+ * child({ path: 'users/:id', component: ({ params }) => <User id={params.id} /> }),
82
+ * ],
83
+ * }),
84
+ * ];
85
+ * ```
86
+ *
87
+ * `children` as a **function** is what carries a parent's parameters down: the
88
+ * builder it is handed knows what the ancestors captured, so the child above
89
+ * sees both. Children as a plain array work too, and see only their own.
90
+ *
91
+ * All of it is types. `route` returns the object it was given, with a
92
+ * function-form `children` called once, and a table written as plain
93
+ * `RouteDefinition[]` still works — its components just see `Params`.
94
+ */
95
+ export declare const route: RouteBuilder<unknown>;
96
+ /** What `route` accepts, with `Own` from its path and `Inherited` from above. */
97
+ interface RouteSpec<Own, Inherited> {
98
+ readonly component?: RouteComponent<Simplify<Inherited & Own> & Params>;
99
+ /** Loaded when the route is first entered; the result is cached. */
100
+ readonly lazy?: () => Promise<LazyModule<Simplify<Inherited & Own> & Params>>;
101
+ /** Shown while `lazy` is loading, instead of the router's own fallback. */
102
+ readonly pending?: () => View;
103
+ readonly children?: readonly RouteDefinition[] | ((child: RouteBuilder<Simplify<Inherited & Own>>) => readonly RouteDefinition[]);
104
+ }
105
+ /** Builds one route, knowing what the routes above it captured. */
106
+ export interface RouteBuilder<Inherited> {
107
+ <const Path extends string>(spec: {
108
+ readonly path: Path;
109
+ } & RouteSpec<ParamsOf<Path>, Inherited>): RouteDefinition;
110
+ /** An index route adds no path, so it captures exactly what its parent did. */
111
+ (spec: {
112
+ readonly index: true;
113
+ } & RouteSpec<unknown, Inherited>): RouteDefinition;
114
+ }
115
+ export {};
116
+ //# 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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"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@firsthandjs/router",
3
+ "version": "0.1.0",
4
+ "description": "Routing for Firsthand: nested routes, params, and route code loaded on demand.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "types": "./dist/index.d.ts",
15
+ "main": "./dist/index.js",
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "dependencies": {
22
+ "@firsthandjs/core": "0.1.0",
23
+ "@firsthandjs/dom": "0.1.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=20.11.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "provenance": true
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/firsthandjs/firsthand.git",
35
+ "directory": "packages/router"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/firsthandjs/firsthand/issues"
39
+ },
40
+ "homepage": "https://github.com/firsthandjs/firsthand#readme",
41
+ "keywords": [
42
+ "firsthand",
43
+ "router",
44
+ "routing",
45
+ "code-splitting",
46
+ "lazy"
47
+ ]
48
+ }