@weftui/router 0.28.0 → 0.30.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/README.md +3 -3
- package/dist/client/index.d.ts +19 -19
- package/dist/client/index.js +16 -16
- package/dist/{compile-DySac7KN.d.ts → compile-DFlX4Atp.d.ts} +39 -59
- package/dist/{href-uvJ6b7zz.js → href-wIrP-_-3.js} +7 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{outlet-saKduDFz.d.ts → outlet-CWpiNOxW.d.ts} +1 -1
- package/dist/{outlet-C9N4a4_F.js → outlet-Crd9mxZA.js} +49 -55
- package/dist/server/index.d.ts +9 -9
- package/dist/server/index.js +10 -10
- package/docs/explanation/boundaries-and-suspense.md +39 -20
- package/docs/explanation/combinator-api.md +14 -12
- package/docs/explanation/reactive-primitives.md +14 -14
- package/docs/explanation/rendering-model.md +20 -18
- package/docs/explanation/services-and-context.md +35 -21
- package/docs/how-to/add-routing.md +361 -60
- package/docs/how-to/author-components.md +42 -30
- package/docs/how-to/compose-behavior-and-markup.md +144 -0
- package/docs/how-to/handle-forms.md +6 -6
- package/docs/how-to/load-async-data.md +15 -13
- package/docs/how-to/load-data-with-rpc.md +34 -32
- package/docs/how-to/provide-services.md +20 -18
- package/docs/how-to/render-keyed-lists.md +14 -12
- package/docs/how-to/render-on-the-server.md +18 -14
- package/docs/how-to/show-navigation-progress.md +12 -10
- package/docs/how-to/split-routes-lazily.md +16 -14
- package/docs/how-to/style-reactively.md +13 -13
- package/docs/how-to/use-element-refs.md +10 -8
- package/docs/index.md +20 -18
- package/docs/reference/core.md +46 -42
- package/docs/reference/dom.md +274 -58
- package/docs/reference/router.md +69 -47
- package/docs/tutorial/01-your-first-app.md +7 -9
- package/docs/tutorial/02-reactivity.md +8 -6
- package/docs/tutorial/03-services-and-async.md +10 -6
- package/docs/tutorial/04-errors-and-server.md +14 -5
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @weftui/router
|
|
2
2
|
|
|
3
|
-
> Universal nested router for [Weft](https://weftui.dev)
|
|
3
|
+
> Universal nested router for [Weft](https://weftui.dev): one route tree, rendered on the server and the client, with type-safe params and `href`s.
|
|
4
4
|
|
|
5
5
|
Maps a URL to a nested page tree that renders identically on the server (`@weftui/router/server`) and the client (`@weftui/router/client`). Route params and query are decoded through [Effect Schema](https://effect.website/docs/schema/introduction/), `href` builds type-safe URLs that round-trip with the matcher, layouts persist across navigations, and `Router.lazy` code-splits a branch while keeping its descriptor eager.
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ Weft tracks Effect 4's beta line. This release is built and tested against `effe
|
|
|
26
26
|
| `Router.params` / `Router.query` | Read the live match (snapshot); `…Stream` variants for reactive query-in-place updates. |
|
|
27
27
|
| `Router.navigating` | Reactive `Idle`/`Navigating` signal for pending UI during deferred-commit navigation. |
|
|
28
28
|
| `href(ref, args)` | Builds a type-safe URL for a leaf route reference. |
|
|
29
|
-
| `RouterApp` / `RouterOutlet` | The universal router root node
|
|
29
|
+
| `RouterApp` / `RouterOutlet` | The universal router root node: render on both server and client. |
|
|
30
30
|
| `RouterLive` (client) | History-backed `Router` layer; also provides the `AppRpcClientTag` seam. |
|
|
31
31
|
| `RouterServer` (server) | `RouterServer.render` / `RouterServer.toWebHandler` for SSR dispatch. |
|
|
32
32
|
|
|
@@ -54,7 +54,7 @@ href(userRoute, { path: { id: 42 } }); // "/users/42"
|
|
|
54
54
|
- Full docs: **https://weftui.dev**
|
|
55
55
|
- `@weftui/router` API reference: **https://weftui.dev/docs/reference/router**
|
|
56
56
|
- Routing guide: **https://weftui.dev/docs/how-to/add-routing**
|
|
57
|
-
- Bundled with this package: see the [`./docs`](./docs) directory in `node_modules/@weftui/router/docs
|
|
57
|
+
- Bundled with this package: see the [`./docs`](./docs) directory in `node_modules/@weftui/router/docs`. The complete tutorial, how-to, explanation, and reference tree ships on disk for offline and agent use.
|
|
58
58
|
|
|
59
59
|
## License
|
|
60
60
|
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { S as Router, b as NavState, i as RouterDef, m as RouteNode, u as Fields, x as NavigateOptions } from "../compile-
|
|
2
|
-
import { n as outletNode, r as HrefArgs, t as RouterApp } from "../outlet-
|
|
1
|
+
import { S as Router, b as NavState, i as RouterDef, m as RouteNode, u as Fields, x as NavigateOptions } from "../compile-DFlX4Atp.js";
|
|
2
|
+
import { n as outletNode, r as HrefArgs, t as RouterApp } from "../outlet-CWpiNOxW.js";
|
|
3
3
|
import { Effect, Layer, Scope } from "effect";
|
|
4
4
|
import { AppRpcClientTag } from "@weftui/core";
|
|
5
5
|
import { RpcGroup } from "effect/unstable/rpc";
|
|
6
6
|
//#region src/client/router-live.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* The residual app services a caller must still provide through the {@link RouterLiveOptions.context}
|
|
9
|
-
* seam
|
|
9
|
+
* seam: a def's aggregate `R` minus the services `RouterLive` already threads
|
|
10
10
|
* (`Router`, `Router.Outlet`, `AppRpcClientTag`). The client mirror of
|
|
11
11
|
* `RouterServer.AppServices`; resolves to `never` for an app with no app-wide service.
|
|
12
12
|
*/
|
|
13
13
|
type AppServices<R> = Exclude<R, Router | Router.Outlet | AppRpcClientTag>;
|
|
14
|
-
/** True only for the exact `any` type
|
|
14
|
+
/** True only for the exact `any` type (a loosely-typed `RouterDef<any, any>`). */
|
|
15
15
|
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
16
16
|
/**
|
|
17
17
|
* Conditionally shapes the `context` field: **required** when the def has statically
|
|
@@ -37,8 +37,8 @@ interface RouterLiveOptions {
|
|
|
37
37
|
/**
|
|
38
38
|
* The app's `Boundary.rpc` foundation: the merged `RpcGroup` contract (shared
|
|
39
39
|
* with the server handler Layer). Backs the {@link AppRpcClientTag} seam so a
|
|
40
|
-
* hydrated boundary refetch
|
|
41
|
-
* network rpc client. Optional: omit when the app has no `Boundary.rpc
|
|
40
|
+
* hydrated boundary refetch (and a client-first SPA mount) resolve over the
|
|
41
|
+
* network rpc client. Optional: omit when the app has no `Boundary.rpc`. Then
|
|
42
42
|
* no network rpc client is built and a stray `Boundary.rpc` fails with a
|
|
43
43
|
* descriptive error.
|
|
44
44
|
*/
|
|
@@ -57,11 +57,11 @@ interface RouterLiveOptions {
|
|
|
57
57
|
* It additionally derives a real {@link RouterHttpApiClient} from `def.httpApi`
|
|
58
58
|
* (over `FetchHttpClient`, `baseUrl` default same-origin) and exposes it on the
|
|
59
59
|
* `Router` service for network work. SPA URL→leaf resolution stays local via the
|
|
60
|
-
* shared {@link match}er
|
|
60
|
+
* shared {@link match}er: both sides read the one `def.httpApi` definition.
|
|
61
61
|
*
|
|
62
|
-
* Alongside `Router` it provides the core {@link AppRpcClientTag} seam
|
|
62
|
+
* Alongside `Router` it provides the core {@link AppRpcClientTag} seam: a
|
|
63
63
|
* **network** flat rpc client (`RpcClient.make` over `layerProtocolHttp` →
|
|
64
|
-
* `POST /_eui/rpc`)
|
|
64
|
+
* `POST /_eui/rpc`), so `@weftui/dom` can resolve a `Boundary.rpc` (hydrated
|
|
65
65
|
* refetch and client-first mount) without depending on this package or
|
|
66
66
|
* `effect/unstable/rpc`.
|
|
67
67
|
*/
|
|
@@ -74,8 +74,8 @@ declare function RouterLive<R>(def: RouterDef<any, R>, options?: RouterLiveOptio
|
|
|
74
74
|
* the href resolves to a route (L1). Modified clicks, non-left buttons,
|
|
75
75
|
* `target=_blank`, `download`, external origins, same-document (hash-only or
|
|
76
76
|
* identical-URL) navigations, and non-matching hrefs fall through to the
|
|
77
|
-
* browser's native handling
|
|
78
|
-
*
|
|
77
|
+
* browser's native handling. In those cases the interceptor leaves
|
|
78
|
+
* `preventDefault` untouched (L2). The listener is removed on scope teardown (L3).
|
|
79
79
|
*
|
|
80
80
|
* @param def - The router definition, used to decide whether an href matches a route.
|
|
81
81
|
* @param navigate - The router's `navigate`, run via the captured runtime on a match.
|
|
@@ -88,10 +88,10 @@ declare function installLinkInterceptor(def: RouterDef, navigate: (to: string) =
|
|
|
88
88
|
* type-safe {@link href} builder. They mirror the History API the client `Router`
|
|
89
89
|
* layer (`RouterLive`) is backed by:
|
|
90
90
|
*
|
|
91
|
-
* - {@link navigate}
|
|
92
|
-
* - {@link push} / {@link replace}
|
|
93
|
-
* - {@link back} / {@link forward}
|
|
94
|
-
* - {@link setQuery} / {@link patchQuery}
|
|
91
|
+
* - {@link navigate}: go to a leaf route reference with typed `{ path, query }`.
|
|
92
|
+
* - {@link push} / {@link replace}: go to a raw `path + search` string.
|
|
93
|
+
* - {@link back} / {@link forward}: step through History (`history.go`).
|
|
94
|
+
* - {@link setQuery} / {@link patchQuery}: change the current route's query in
|
|
95
95
|
* place, re-encoding through the matched leaf's `querySchema`.
|
|
96
96
|
*
|
|
97
97
|
* All but `back`/`forward` require the `Router` service (run them within the layer
|
|
@@ -99,10 +99,10 @@ declare function installLinkInterceptor(def: RouterDef, navigate: (to: string) =
|
|
|
99
99
|
*/
|
|
100
100
|
/**
|
|
101
101
|
* Navigates to a leaf route `ref` with typed `path`/`query` args, building the URL
|
|
102
|
-
* via {@link href} (so it round-trips with `match`) and pushing
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
102
|
+
* via {@link href} (so it round-trips with `match`) and pushing the History entry,
|
|
103
|
+
* or replacing it with `options.replace`. `path` is required when the route has
|
|
104
|
+
* path params; `query` is optional when every query field is optional (same
|
|
105
|
+
* requiredness rules as `href`).
|
|
106
106
|
*
|
|
107
107
|
* @example
|
|
108
108
|
* ```ts
|
package/dist/client/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as Router, i as setResolvedCommit, n as outletNode, o as getPreload, r as preRunLeaf, t as RouterApp } from "../outlet-
|
|
2
|
-
import { r as match, t as href } from "../href-
|
|
1
|
+
import { a as Router, i as setResolvedCommit, n as outletNode, o as getPreload, r as preRunLeaf, t as RouterApp } from "../outlet-Crd9mxZA.js";
|
|
2
|
+
import { r as match, t as href } from "../href-wIrP-_-3.js";
|
|
3
3
|
import { HttpApiClient } from "effect/unstable/httpapi";
|
|
4
4
|
import { Context, Effect, Exit, Fiber, Layer, Option, Schema, Scope, Stream, SubscriptionRef } from "effect";
|
|
5
5
|
import { AppRpcClientTag, Subscribable } from "@weftui/core";
|
|
@@ -11,8 +11,8 @@ import { FetchHttpClient } from "effect/unstable/http";
|
|
|
11
11
|
* the href resolves to a route (L1). Modified clicks, non-left buttons,
|
|
12
12
|
* `target=_blank`, `download`, external origins, same-document (hash-only or
|
|
13
13
|
* identical-URL) navigations, and non-matching hrefs fall through to the
|
|
14
|
-
* browser's native handling
|
|
15
|
-
*
|
|
14
|
+
* browser's native handling. In those cases the interceptor leaves
|
|
15
|
+
* `preventDefault` untouched (L2). The listener is removed on scope teardown (L3).
|
|
16
16
|
*
|
|
17
17
|
* @param def - The router definition, used to decide whether an href matches a route.
|
|
18
18
|
* @param navigate - The router's `navigate`, run via the captured runtime on a match.
|
|
@@ -68,11 +68,11 @@ function normalizeTo(to) {
|
|
|
68
68
|
* It additionally derives a real {@link RouterHttpApiClient} from `def.httpApi`
|
|
69
69
|
* (over `FetchHttpClient`, `baseUrl` default same-origin) and exposes it on the
|
|
70
70
|
* `Router` service for network work. SPA URL→leaf resolution stays local via the
|
|
71
|
-
* shared {@link match}er
|
|
71
|
+
* shared {@link match}er: both sides read the one `def.httpApi` definition.
|
|
72
72
|
*
|
|
73
|
-
* Alongside `Router` it provides the core {@link AppRpcClientTag} seam
|
|
73
|
+
* Alongside `Router` it provides the core {@link AppRpcClientTag} seam: a
|
|
74
74
|
* **network** flat rpc client (`RpcClient.make` over `layerProtocolHttp` →
|
|
75
|
-
* `POST /_eui/rpc`)
|
|
75
|
+
* `POST /_eui/rpc`), so `@weftui/dom` can resolve a `Boundary.rpc` (hydrated
|
|
76
76
|
* refetch and client-first mount) without depending on this package or
|
|
77
77
|
* `effect/unstable/rpc`.
|
|
78
78
|
*/
|
|
@@ -187,10 +187,10 @@ function RouterLive(def, options = {}) {
|
|
|
187
187
|
* type-safe {@link href} builder. They mirror the History API the client `Router`
|
|
188
188
|
* layer (`RouterLive`) is backed by:
|
|
189
189
|
*
|
|
190
|
-
* - {@link navigate}
|
|
191
|
-
* - {@link push} / {@link replace}
|
|
192
|
-
* - {@link back} / {@link forward}
|
|
193
|
-
* - {@link setQuery} / {@link patchQuery}
|
|
190
|
+
* - {@link navigate}: go to a leaf route reference with typed `{ path, query }`.
|
|
191
|
+
* - {@link push} / {@link replace}: go to a raw `path + search` string.
|
|
192
|
+
* - {@link back} / {@link forward}: step through History (`history.go`).
|
|
193
|
+
* - {@link setQuery} / {@link patchQuery}: change the current route's query in
|
|
194
194
|
* place, re-encoding through the matched leaf's `querySchema`.
|
|
195
195
|
*
|
|
196
196
|
* All but `back`/`forward` require the `Router` service (run them within the layer
|
|
@@ -198,10 +198,10 @@ function RouterLive(def, options = {}) {
|
|
|
198
198
|
*/
|
|
199
199
|
/**
|
|
200
200
|
* Navigates to a leaf route `ref` with typed `path`/`query` args, building the URL
|
|
201
|
-
* via {@link href} (so it round-trips with `match`) and pushing
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
201
|
+
* via {@link href} (so it round-trips with `match`) and pushing the History entry,
|
|
202
|
+
* or replacing it with `options.replace`. `path` is required when the route has
|
|
203
|
+
* path params; `query` is optional when every query field is optional (same
|
|
204
|
+
* requiredness rules as `href`).
|
|
205
205
|
*
|
|
206
206
|
* @example
|
|
207
207
|
* ```ts
|
|
@@ -245,7 +245,7 @@ function pathOf(url) {
|
|
|
245
245
|
function applyQuery(transform, options) {
|
|
246
246
|
return Effect.gen(function* () {
|
|
247
247
|
const router = yield* Router;
|
|
248
|
-
const match = yield* router.currentMatch
|
|
248
|
+
const match = yield* Subscribable.get(router.currentMatch);
|
|
249
249
|
if (match._tag !== "Matched") return;
|
|
250
250
|
const next = transform(match.query);
|
|
251
251
|
const search = encodeSearch(Schema.encodeUnknownSync(match.leaf.querySchema)(next));
|
|
@@ -35,7 +35,7 @@ declare const RouterParamsError_base: Schema.Class<RouterParamsError, Schema.Tag
|
|
|
35
35
|
}>, import("effect/Cause").YieldableError>;
|
|
36
36
|
/**
|
|
37
37
|
* Tagged error raised by `Router.params` / `Router.query` when the live match does
|
|
38
|
-
* not satisfy the requested fields
|
|
38
|
+
* not satisfy the requested fields: either no route is matched, or a requested
|
|
39
39
|
* key is missing / fails its schema's `Type`-side validation. `source` records
|
|
40
40
|
* whether the failure was on the path params or the query, and `keys` lists the
|
|
41
41
|
* requested field names for diagnostics.
|
|
@@ -76,19 +76,16 @@ interface MatcherEntry {
|
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
78
|
* Precompiles a {@link RouterDef} into ordered matcher entries (memoized per
|
|
79
|
-
* `RouterDef`).
|
|
80
|
-
* `def.httpApi` `"pages"` endpoints
|
|
81
|
-
* also reads
|
|
79
|
+
* `RouterDef`). Patterns and path/query schemas are read from the authoritative
|
|
80
|
+
* `def.httpApi` `"pages"` endpoints (the single source of truth the server dispatch
|
|
81
|
+
* also reads), and each entry's render metadata leaf is resolved from `def.compiled`
|
|
82
82
|
* by endpoint id. Matching stays local (SPA URL→leaf); see the refactor plan's
|
|
83
83
|
* _Feasibility constraint_.
|
|
84
84
|
*
|
|
85
85
|
* Entries are sorted most-specific first (fewer params, then longer pattern) so a
|
|
86
|
-
* static segment wins over a param segment at the same position (M6).
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* It resolves the common "static beats param at the same position" case, but two
|
|
90
|
-
* patterns with the same param count and length (e.g. `/a/:b/c` vs `/a/x/:d`)
|
|
91
|
-
* fall back to endpoint order.
|
|
86
|
+
* static segment wins over a param segment at the same position (M6). That order is
|
|
87
|
+
* a global heuristic, so two patterns with the same param count and length (e.g.
|
|
88
|
+
* `/a/:b/c` vs `/a/x/:d`) fall back to endpoint order.
|
|
92
89
|
*/
|
|
93
90
|
declare function compileMatchers(def: RouterDef): readonly MatcherEntry[];
|
|
94
91
|
/**
|
|
@@ -101,7 +98,7 @@ declare function match(def: RouterDef, url: string): RouteMatch;
|
|
|
101
98
|
//#endregion
|
|
102
99
|
//#region src/router-service.d.ts
|
|
103
100
|
/**
|
|
104
|
-
* The universal router service. Provided per render
|
|
101
|
+
* The universal router service. Provided per render: by `RouterLive` on the
|
|
105
102
|
* client (History-API backed) and by a fixed per-request implementation on the
|
|
106
103
|
* server. Layouts and pages read it anywhere via `yield* Router`.
|
|
107
104
|
*
|
|
@@ -109,12 +106,12 @@ declare function match(def: RouterDef, url: string): RouteMatch;
|
|
|
109
106
|
* {@link Router.layout}, and {@link Router.router} build the route tree (mirroring
|
|
110
107
|
* `Component.gen` / `Boundary.catchTag` / `h.div`), and {@link Router.Outlet} /
|
|
111
108
|
* {@link Router.params} / {@link Router.query} deliver the outlet and the live
|
|
112
|
-
* match's params/query by dependency injection. The roles merge by declaration
|
|
109
|
+
* match's params/query by dependency injection. The roles merge by declaration:
|
|
113
110
|
* `yield* Router` reads the service; `Router.route(…)` authors a tree.
|
|
114
111
|
*/
|
|
115
112
|
/**
|
|
116
113
|
* The platform `HttpApiClient` derived from a router's `HttpApi` spine. Typed
|
|
117
|
-
* opaquely (`Client<any, …>`) because the spine is `HttpApi.Top
|
|
114
|
+
* opaquely (`Client<any, …>`) because the spine is `HttpApi.Top`: its
|
|
118
115
|
* group/endpoint shapes are assembled in a runtime loop by `buildHttpApi`, so a
|
|
119
116
|
* precise client type is not recoverable. Present (`Option.some`) on the client
|
|
120
117
|
* (`RouterLive`), absent (`Option.none`) on the server, which is itself the origin.
|
|
@@ -156,8 +153,8 @@ declare const Router_base: Context.ServiceClass<Router, "@weftui/router/Router",
|
|
|
156
153
|
/**
|
|
157
154
|
* The derived {@link RouterHttpApiClient} for network work (route prefetch,
|
|
158
155
|
* foundation for future loaders/data). `Option.some` on the client,
|
|
159
|
-
* `Option.none` on the server. SPA URL→leaf resolution does **not** use this
|
|
160
|
-
*
|
|
156
|
+
* `Option.none` on the server. SPA URL→leaf resolution does **not** use this.
|
|
157
|
+
* It stays local via the shared matcher (see the refactor _Feasibility constraint_).
|
|
161
158
|
*/
|
|
162
159
|
readonly httpApiClient: Option.Option<RouterHttpApiClient>;
|
|
163
160
|
/**
|
|
@@ -178,17 +175,17 @@ declare const OutletTag_base: Context.ServiceClass<OutletTag, "@weftui/router/Ou
|
|
|
178
175
|
* reads it with `yield* Router.Outlet`.
|
|
179
176
|
*
|
|
180
177
|
* Typed **opaque** as `Node<never, never>` so splicing `[outlet]` adds nothing to
|
|
181
|
-
* a layout's local channels
|
|
178
|
+
* a layout's local channels: the subtree's real channels are aggregated
|
|
182
179
|
* structurally by {@link makeLayout} / {@link makeRouter}, never inferred across
|
|
183
180
|
* this DI boundary. Re-exported on the namespace as `Router.Outlet`.
|
|
184
181
|
*/
|
|
185
182
|
declare class OutletTag extends OutletTag_base {}
|
|
186
183
|
/**
|
|
187
184
|
* Reads the live match's **path params** for the requested `fields`. Snapshot
|
|
188
|
-
* semantics
|
|
185
|
+
* semantics (reads `yield* Router`, then `Subscribable.get(currentMatch)`), returning the
|
|
189
186
|
* already-decoded values **directly**: the matcher decoded them against the leaf's
|
|
190
|
-
* full path schema, so no re-validation is needed
|
|
191
|
-
* subset is the `Type` side of `fields
|
|
187
|
+
* full path schema, so no re-validation is needed. The cast is sound because the
|
|
188
|
+
* picked subset is the `Type` side of `fields`. Fails with a {@link RouterParamsError}
|
|
192
189
|
* (`source: "path"`) only when no route is matched. Re-exported as `Router.params`.
|
|
193
190
|
*/
|
|
194
191
|
declare function readParams<F extends Fields>(fields: F): Effect.Effect<FieldsType<F>, RouterParamsError, Router>;
|
|
@@ -201,9 +198,9 @@ declare function readParams<F extends Fields>(fields: F): Effect.Effect<FieldsTy
|
|
|
201
198
|
declare function readQuery<F extends Fields>(fields: F): Effect.Effect<FieldsType<F>, RouterParamsError, Router>;
|
|
202
199
|
/**
|
|
203
200
|
* Reactive counterpart to {@link readParams}: a {@link Subscribable} of the live
|
|
204
|
-
* match's **path params** for `fields`, derived from `
|
|
201
|
+
* match's **path params** for `fields`, derived from `Subscribable.changes(currentMatch)`. It
|
|
205
202
|
* re-emits on every navigation and stays live across `NotFound` (yielding the empty
|
|
206
|
-
* subset), so a component can render `[(yield* Router.paramsStream(fields))
|
|
203
|
+
* subset), so a component can render `[Subscribable.changes(yield* Router.paramsStream(fields))]`
|
|
207
204
|
* and update in place even when the outlet keeps the same leaf mounted. Re-exported
|
|
208
205
|
* as `Router.paramsStream`.
|
|
209
206
|
*/
|
|
@@ -252,7 +249,7 @@ type FieldsType<F extends Fields> = Schema.Struct.Type<F>;
|
|
|
252
249
|
* The router passes `{ path, query }` into the leaf slot at render time (the
|
|
253
250
|
* {@link makeRoute} props-form overload), so a page can read them directly as props
|
|
254
251
|
* instead of via the `Router.params` / `Router.query` dependency-injection
|
|
255
|
-
* accessors. Layouts and deeper nodes
|
|
252
|
+
* accessors. Layouts and deeper nodes keep DI, since they can't take handler args.
|
|
256
253
|
*/
|
|
257
254
|
interface RouteHandlerProps<Path extends Fields = {}, Query extends Fields = {}> {
|
|
258
255
|
/** The live match's decoded path params (`Type` side of the route's `path`). */
|
|
@@ -264,26 +261,26 @@ interface RouteHandlerProps<Path extends Fields = {}, Query extends Fields = {}>
|
|
|
264
261
|
* The shape of a route/layout `component` slot: a callable producing a {@link Node},
|
|
265
262
|
* invoked by the router at render time. It accepts both a plain zero-arg thunk
|
|
266
263
|
* (`() => h.div(…)`) and a {@link Component} produced by `Component.make` /
|
|
267
|
-
* `Component.gen` (a generic `(props, children?) => Node`). The `props: any` arm
|
|
268
|
-
* rather than `()
|
|
264
|
+
* `Component.gen` (a generic `(props, children?) => Node`). The `props: any` arm,
|
|
265
|
+
* rather than `()`, is what keeps a required-props `Component<…>` structurally
|
|
269
266
|
* assignable; the router calls the slot with no arguments.
|
|
270
267
|
*/
|
|
271
268
|
type ComponentSlot<N extends Node<any, any> = Node<any, any>> = (props: any) => N;
|
|
272
269
|
/**
|
|
273
270
|
* The {@link Node} a {@link ComponentSlot} produces when the router invokes it with no
|
|
274
|
-
* props/children
|
|
271
|
+
* props/children. Used to recover the slot's `E`/`R` channels for the route tree.
|
|
275
272
|
*
|
|
276
273
|
* A plain zero-arg thunk is matched first (`() => infer N`): a required-props
|
|
277
274
|
* `Component` is *not* assignable to `() => unknown`, so it falls through to the
|
|
278
275
|
* `Component` arm, where the internal `E`/`R` type parameters are read directly. This
|
|
279
|
-
* two-step form is deliberate
|
|
276
|
+
* two-step form is deliberate: `ReturnType<S>` collapses a generic `Component`'s
|
|
280
277
|
* channels to `unknown` (they depend on the erased `GenP`/`GenC`), whereas extracting
|
|
281
278
|
* the `Component<…, E, R>` parameters preserves them. Caller prop/children channels are
|
|
282
279
|
* never relevant here because the router supplies neither.
|
|
283
280
|
*/
|
|
284
281
|
type SlotNode<S> = S extends (() => infer N) ? N : S extends Component.Component<any, any, infer E, infer R> ? Node<E, R> : never;
|
|
285
282
|
/**
|
|
286
|
-
* A leaf page in the route tree. Its `component` *is* its handler
|
|
283
|
+
* A leaf page in the route tree. Its `component` *is* its handler: a
|
|
287
284
|
* {@link ComponentSlot} (a `Component.gen` / `Component.make` component, or a plain
|
|
288
285
|
* `() => Node` thunk) that the router invokes at render time and that reads the live
|
|
289
286
|
* match's params via `Router.params` / `Router.query`. `Path`/`Query` drive matching
|
|
@@ -304,7 +301,7 @@ interface RouteNode<Path extends Fields = {}, Query extends Fields = {}, E = nev
|
|
|
304
301
|
}
|
|
305
302
|
/**
|
|
306
303
|
* A layout wrapping an outlet (the next level down) in the route tree. A layout is
|
|
307
|
-
* **purely UI nesting
|
|
304
|
+
* **purely UI nesting**: it owns **no path or segment**; all path structure lives
|
|
308
305
|
* on routes. Its `component` is a {@link ComponentSlot} that splices the injected
|
|
309
306
|
* outlet via `yield* Router.Outlet`; the router invokes it per render and discharges
|
|
310
307
|
* that `Outlet` requirement. A layout that needs a param reads it via `Router.params`.
|
|
@@ -319,7 +316,7 @@ interface LayoutNode<E = never, R = never> {
|
|
|
319
316
|
/**
|
|
320
317
|
* Phantom marker for this layout subtree's aggregate error channel (see
|
|
321
318
|
* {@link TreeE}). Covariant (stores `E` directly) so a fully-discharged layout
|
|
322
|
-
* (`LayoutNode<never, never
|
|
319
|
+
* (`LayoutNode<never, never>`, its `Outlet` provided and no subtree errors) stays
|
|
323
320
|
* assignable to the `LayoutNode<any, any>` arm of {@link TreeNode}.
|
|
324
321
|
*/
|
|
325
322
|
readonly _E?: E;
|
|
@@ -337,26 +334,17 @@ type SubtreeE<C extends readonly TreeNode[]> = TreeE<C[number]>;
|
|
|
337
334
|
/** Aggregate requirement channel over a children tuple (distributes over `C[number]`). */
|
|
338
335
|
type SubtreeR<C extends readonly TreeNode[]> = TreeR<C[number]>;
|
|
339
336
|
/**
|
|
340
|
-
* Declares a leaf page. The `component` *is* the route handler
|
|
337
|
+
* Declares a leaf page. The `component` *is* the route handler: a thunk the
|
|
341
338
|
* router invokes at render time; its error / requirement channels propagate up the
|
|
342
339
|
* tree. Two authoring forms are accepted:
|
|
343
340
|
*
|
|
344
|
-
* - **Handler-arg props
|
|
341
|
+
* - **Handler-arg props**: the slot declares `(props: {@link RouteHandlerProps})`
|
|
345
342
|
* and the router passes the live match's decoded `{ path, query }` in directly
|
|
346
343
|
* (first overload; `path`/`query` are inferred from the route's `path`/`query`
|
|
347
|
-
* fields). A plain zero-arg thunk works too
|
|
348
|
-
* - **Dependency injection
|
|
344
|
+
* fields). A plain zero-arg thunk works too. It just ignores the props.
|
|
345
|
+
* - **Dependency injection**: a `Component.make` / `Component.gen` component that
|
|
349
346
|
* reads the live match via `Router.params` / `Router.query` (second overload).
|
|
350
347
|
*
|
|
351
|
-
* @example Handler-arg props (decoded `{ path, query }`)
|
|
352
|
-
* ```ts
|
|
353
|
-
* Router.route("users/:id", {
|
|
354
|
-
* path: { id: Schema.NumberFromString },
|
|
355
|
-
* query: { tab: Schema.optional(Schema.String) },
|
|
356
|
-
* component: ({ path, query }) => h.div({}, `User ${path.id} (${query.tab ?? "info"})`),
|
|
357
|
-
* });
|
|
358
|
-
* ```
|
|
359
|
-
*
|
|
360
348
|
* @example Dependency injection (`Router.params` / a `Component`)
|
|
361
349
|
* ```ts
|
|
362
350
|
* Router.route("about", { component: Component.make(() => h.h1({}, "About")) });
|
|
@@ -404,24 +392,16 @@ declare function makeLayout<C extends readonly TreeNode[], S extends ComponentSl
|
|
|
404
392
|
}, children: C): LayoutNode<Node.Error<SlotNode<S>> | SubtreeE<C>, Exclude<Node.Context<SlotNode<S>>, Router.Outlet> | SubtreeR<C>>;
|
|
405
393
|
/**
|
|
406
394
|
* Wraps a dynamic-import `load` as a lazy {@link ComponentSlot}: the route's descriptor
|
|
407
|
-
* (`segment`, `path`/`query`) stays eager and matchable, while the component
|
|
408
|
-
* body and its module's deps
|
|
395
|
+
* (`segment`, `path`/`query`) stays eager and matchable, while the component (the render
|
|
396
|
+
* body and its module's deps) is split into the chunk `load` resolves. The router invokes
|
|
409
397
|
* the returned slot at render time; it awaits `load` then renders the resolved component,
|
|
410
398
|
* adopting the server DOM in place on hydration (flash-free) and fetching the chunk on
|
|
411
399
|
* client navigation. Exposed as {@link Router.lazy}. See `lazy-component.specs.md`.
|
|
412
400
|
*
|
|
413
401
|
* The resolved value is a component slot (`Component.gen` / `Component.make`, or a
|
|
414
|
-
* `() => Node` thunk)
|
|
402
|
+
* `() => Node` thunk), the shape `component:` already accepts, so its `E`/`R` channels
|
|
415
403
|
* are recovered via {@link SlotNode} and propagate up the tree exactly as an eager
|
|
416
404
|
* component's do.
|
|
417
|
-
*
|
|
418
|
-
* @example
|
|
419
|
-
* ```ts
|
|
420
|
-
* Router.route("docs/:category/:slug", {
|
|
421
|
-
* path: { category: Schema.String, slug: Schema.String },
|
|
422
|
-
* component: Router.lazy(() => import("./doc-page").then((m) => m.DocPage)),
|
|
423
|
-
* });
|
|
424
|
-
* ```
|
|
425
405
|
*/
|
|
426
406
|
declare function lazyComponent<S extends ComponentSlot>(load: () => Promise<S>): () => Node<Node.Error<SlotNode<S>>, Node.Context<SlotNode<S>>>;
|
|
427
407
|
//#endregion
|
|
@@ -430,7 +410,7 @@ declare function lazyComponent<S extends ComponentSlot>(load: () => Promise<S>):
|
|
|
430
410
|
* A compiled layout level: its component slot plus the dedupe `patternPrefix` used
|
|
431
411
|
* by the client outlet to key the level. A layout owns no path of its own, so the
|
|
432
412
|
* prefix is derived as the **longest common path-segment prefix of every leaf in
|
|
433
|
-
* the layout's subtree
|
|
413
|
+
* the layout's subtree**: it changes (and the level re-renders) exactly when a
|
|
434
414
|
* param shared by all those leaves changes, and persists otherwise.
|
|
435
415
|
*/
|
|
436
416
|
interface CompiledLayout {
|
|
@@ -456,7 +436,7 @@ interface CompiledLeaf {
|
|
|
456
436
|
/**
|
|
457
437
|
* Path-param schema. Its **encoded** side is typed string-encodeable
|
|
458
438
|
* (`Record<string, string | undefined>`) so it satisfies platform's
|
|
459
|
-
* `HttpApiEndpoint` `params` constraint without an `as any` cast
|
|
439
|
+
* `HttpApiEndpoint` `params` constraint without an `as any` cast: param schemas
|
|
460
440
|
* round-trip strings, so the `Schema.Struct` value is asserted to this shape.
|
|
461
441
|
*/
|
|
462
442
|
readonly pathSchema: Schema.Codec<Record<string, unknown>, Readonly<Record<string, string | undefined>>>;
|
|
@@ -520,9 +500,9 @@ declare const leafRegistry: WeakMap<RouteNode<any, any, any, any>, CompiledLeaf>
|
|
|
520
500
|
* Pass 1 walks the tree: only **routes** contribute path parts (layouts own no
|
|
521
501
|
* path), so each leaf's `parts` come solely from the route segments on its branch,
|
|
522
502
|
* and its ancestor `LayoutNode`s are recorded in order. Pass 2 derives one shared
|
|
523
|
-
* {@link CompiledLayout} per distinct layout node
|
|
524
|
-
* longest common path prefix of that layout's subtree leaves
|
|
525
|
-
* leaf's `layoutChain` (root → parent) and merged path schema.
|
|
503
|
+
* {@link CompiledLayout} per distinct layout node, whose `patternPrefix` is the
|
|
504
|
+
* longest common path prefix of that layout's subtree leaves. It then assembles
|
|
505
|
+
* each leaf's `layoutChain` (root → parent) and merged path schema.
|
|
526
506
|
*/
|
|
527
507
|
declare function compile(def: {
|
|
528
508
|
root: TreeNode;
|
|
@@ -530,7 +510,7 @@ declare function compile(def: {
|
|
|
530
510
|
}): Compiled;
|
|
531
511
|
/**
|
|
532
512
|
* Builds the authoritative `HttpApi` for a compiled tree (S4): a single `"pages"`
|
|
533
|
-
* group
|
|
513
|
+
* group holding one GET endpoint per leaf, at each leaf's full path
|
|
534
514
|
* pattern, carrying `params: pathSchema`, `query: querySchema`, a
|
|
535
515
|
* `Schema.String` (text/HTML) success, and a `RouterNotFound → 404` error. The tree
|
|
536
516
|
* (not `HttpApi`) is the authoring surface; this is the single source of truth the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as leafRegistry } from "./outlet-
|
|
1
|
+
import { l as leafRegistry } from "./outlet-Crd9mxZA.js";
|
|
2
2
|
import { Result, Schema } from "effect";
|
|
3
3
|
//#region src/matcher.ts
|
|
4
4
|
/** Escapes a literal path segment for inclusion in a `RegExp`. */
|
|
@@ -28,19 +28,16 @@ const emptySchema = Schema.Struct({});
|
|
|
28
28
|
const matchersCache = /* @__PURE__ */ new WeakMap();
|
|
29
29
|
/**
|
|
30
30
|
* Precompiles a {@link RouterDef} into ordered matcher entries (memoized per
|
|
31
|
-
* `RouterDef`).
|
|
32
|
-
* `def.httpApi` `"pages"` endpoints
|
|
33
|
-
* also reads
|
|
31
|
+
* `RouterDef`). Patterns and path/query schemas are read from the authoritative
|
|
32
|
+
* `def.httpApi` `"pages"` endpoints (the single source of truth the server dispatch
|
|
33
|
+
* also reads), and each entry's render metadata leaf is resolved from `def.compiled`
|
|
34
34
|
* by endpoint id. Matching stays local (SPA URL→leaf); see the refactor plan's
|
|
35
35
|
* _Feasibility constraint_.
|
|
36
36
|
*
|
|
37
37
|
* Entries are sorted most-specific first (fewer params, then longer pattern) so a
|
|
38
|
-
* static segment wins over a param segment at the same position (M6).
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* It resolves the common "static beats param at the same position" case, but two
|
|
42
|
-
* patterns with the same param count and length (e.g. `/a/:b/c` vs `/a/x/:d`)
|
|
43
|
-
* fall back to endpoint order.
|
|
38
|
+
* static segment wins over a param segment at the same position (M6). That order is
|
|
39
|
+
* a global heuristic, so two patterns with the same param count and length (e.g.
|
|
40
|
+
* `/a/:b/c` vs `/a/x/:d`) fall back to endpoint order.
|
|
44
41
|
*/
|
|
45
42
|
function compileMatchers(def) {
|
|
46
43
|
const cached = matchersCache.get(def);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as notFound, C as RouterHttpApiClient, D as RouterNotFound, E as match, O as RouterParamsError, S as Router, T as compileMatchers, _ as TreeE, a as RouterOptions, b as NavState, c as leafRegistry, d as FieldsType, f as LayoutNode, g as SubtreeR, h as SubtreeE, i as RouterDef, k as isRouterNotFound, l as ComponentSlot, m as RouteNode, n as CompiledLayout, o as buildHttpApi, p as RouteHandlerProps, r as CompiledLeaf, s as compile, t as Compiled, u as Fields, v as TreeNode, w as RouteMatch, x as NavigateOptions, y as TreeR } from "./compile-
|
|
2
|
-
import { i as href, n as outletNode, r as HrefArgs, t as RouterApp } from "./outlet-
|
|
1
|
+
import { A as notFound, C as RouterHttpApiClient, D as RouterNotFound, E as match, O as RouterParamsError, S as Router, T as compileMatchers, _ as TreeE, a as RouterOptions, b as NavState, c as leafRegistry, d as FieldsType, f as LayoutNode, g as SubtreeR, h as SubtreeE, i as RouterDef, k as isRouterNotFound, l as ComponentSlot, m as RouteNode, n as CompiledLayout, o as buildHttpApi, p as RouteHandlerProps, r as CompiledLeaf, s as compile, t as Compiled, u as Fields, v as TreeNode, w as RouteMatch, x as NavigateOptions, y as TreeR } from "./compile-DFlX4Atp.js";
|
|
2
|
+
import { i as href, n as outletNode, r as HrefArgs, t as RouterApp } from "./outlet-CWpiNOxW.js";
|
|
3
3
|
export { type Compiled, type CompiledLayout, type CompiledLeaf, type ComponentSlot, type Fields, type FieldsType, type HrefArgs, type LayoutNode, type NavState, type NavigateOptions, type RouteHandlerProps, type RouteMatch, type RouteNode, Router, RouterApp, type RouterDef, type RouterHttpApiClient, RouterNotFound, type RouterOptions, RouterParamsError, type SubtreeE, type SubtreeR, type TreeE, type TreeNode, type TreeR, buildHttpApi, compile, compileMatchers, href, isRouterNotFound, leafRegistry, match, notFound, outletNode };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as Router, c as compile, d as RouterParamsError, f as isRouterNotFound, l as leafRegistry, n as outletNode, p as notFound, s as buildHttpApi, t as RouterApp, u as RouterNotFound } from "./outlet-
|
|
2
|
-
import { n as compileMatchers, r as match, t as href } from "./href-
|
|
1
|
+
import { a as Router, c as compile, d as RouterParamsError, f as isRouterNotFound, l as leafRegistry, n as outletNode, p as notFound, s as buildHttpApi, t as RouterApp, u as RouterNotFound } from "./outlet-Crd9mxZA.js";
|
|
2
|
+
import { n as compileMatchers, r as match, t as href } from "./href-wIrP-_-3.js";
|
|
3
3
|
export { Router, RouterApp, RouterNotFound, RouterParamsError, buildHttpApi, compile, compileMatchers, href, isRouterNotFound, leafRegistry, match, notFound, outletNode };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as RouterNotFound, S as Router, d as FieldsType, i as RouterDef, m as RouteNode, u as Fields } from "./compile-
|
|
1
|
+
import { D as RouterNotFound, S as Router, d as FieldsType, i as RouterDef, m as RouteNode, u as Fields } from "./compile-DFlX4Atp.js";
|
|
2
2
|
import { Node } from "@weftui/core";
|
|
3
3
|
//#region src/href.d.ts
|
|
4
4
|
/**
|