@weftui/router 0.27.1 → 0.29.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 +21 -21
- package/dist/client/index.js +15 -15
- package/dist/{compile-BJFIgBbE.d.ts → compile-Bb7AknG_.d.ts} +46 -62
- package/dist/{href-uvJ6b7zz.js → href-3swSSbn_.js} +7 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{outlet-eoUQ-W0k.d.ts → outlet-2AnUWKQD.d.ts} +1 -2
- package/dist/{outlet-C9N4a4_F.js → outlet-BgJmsO_g.js} +38 -46
- package/dist/server/index.d.ts +9 -10
- package/dist/server/index.js +10 -10
- package/docs/explanation/boundaries-and-suspense.md +37 -18
- package/docs/explanation/combinator-api.md +14 -12
- package/docs/explanation/reactive-primitives.md +15 -15
- package/docs/explanation/rendering-model.md +20 -18
- package/docs/explanation/services-and-context.md +39 -29
- package/docs/how-to/add-routing.md +76 -52
- package/docs/how-to/author-components.md +46 -32
- 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 -30
- package/docs/how-to/provide-services.md +54 -74
- package/docs/how-to/render-keyed-lists.md +10 -8
- package/docs/how-to/render-on-the-server.md +19 -14
- package/docs/how-to/show-navigation-progress.md +10 -8
- 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 +44 -40
- package/docs/reference/dom.md +395 -58
- package/docs/reference/router.md +71 -49
- package/docs/tutorial/01-your-first-app.md +10 -11
- package/docs/tutorial/02-reactivity.md +11 -8
- package/docs/tutorial/03-services-and-async.md +19 -13
- package/docs/tutorial/04-errors-and-server.md +17 -7
- package/package.json +10 -9
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,18 +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-Bb7AknG_.js";
|
|
2
|
+
import { n as outletNode, r as HrefArgs, t as RouterApp } from "../outlet-2AnUWKQD.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
|
-
|
|
7
6
|
//#region src/client/router-live.d.ts
|
|
8
7
|
/**
|
|
9
8
|
* The residual app services a caller must still provide through the {@link RouterLiveOptions.context}
|
|
10
|
-
* seam
|
|
9
|
+
* seam: a def's aggregate `R` minus the services `RouterLive` already threads
|
|
11
10
|
* (`Router`, `Router.Outlet`, `AppRpcClientTag`). The client mirror of
|
|
12
11
|
* `RouterServer.AppServices`; resolves to `never` for an app with no app-wide service.
|
|
13
12
|
*/
|
|
14
13
|
type AppServices<R> = Exclude<R, Router | Router.Outlet | AppRpcClientTag>;
|
|
15
|
-
/** True only for the exact `any` type
|
|
14
|
+
/** True only for the exact `any` type (a loosely-typed `RouterDef<any, any>`). */
|
|
16
15
|
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
17
16
|
/**
|
|
18
17
|
* Conditionally shapes the `context` field: **required** when the def has statically
|
|
@@ -38,13 +37,14 @@ interface RouterLiveOptions {
|
|
|
38
37
|
/**
|
|
39
38
|
* The app's `Boundary.rpc` foundation: the merged `RpcGroup` contract (shared
|
|
40
39
|
* with the server handler Layer). Backs the {@link AppRpcClientTag} seam so a
|
|
41
|
-
* hydrated boundary refetch
|
|
42
|
-
* 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
|
|
43
42
|
* no network rpc client is built and a stray `Boundary.rpc` fails with a
|
|
44
43
|
* descriptive error.
|
|
45
44
|
*/
|
|
46
45
|
readonly rpc?: {
|
|
47
|
-
/** The app's merged `RpcGroup` (pure Schema contract). */
|
|
46
|
+
/** The app's merged `RpcGroup` (pure Schema contract). */
|
|
47
|
+
readonly group: RpcGroup.RpcGroup<any>;
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
@@ -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-BgJmsO_g.js";
|
|
2
|
+
import { r as match, t as href } from "../href-3swSSbn_.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
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi";
|
|
2
2
|
import { Context, Effect, Option, Schema } from "effect";
|
|
3
3
|
import { Component, Node, Subscribable } from "@weftui/core";
|
|
4
|
-
|
|
5
4
|
//#region src/errors.d.ts
|
|
6
5
|
declare const RouterNotFound_base: Schema.Class<RouterNotFound, Schema.TaggedStruct<"RouterNotFound", {
|
|
7
|
-
/** The path that could not be resolved, when known. */
|
|
6
|
+
/** The path that could not be resolved, when known. */
|
|
7
|
+
readonly path: Schema.optional<Schema.String>;
|
|
8
8
|
}>, import("effect/Cause").YieldableError>;
|
|
9
9
|
/**
|
|
10
10
|
* Tagged error raised by {@link notFound} and caught by the router's internal
|
|
@@ -28,12 +28,14 @@ declare const notFound: (path?: string) => Effect.Effect<never, RouterNotFound>;
|
|
|
28
28
|
/** Type guard recognising a {@link RouterNotFound} value regardless of its prototype. */
|
|
29
29
|
declare const isRouterNotFound: (u: unknown) => u is RouterNotFound;
|
|
30
30
|
declare const RouterParamsError_base: Schema.Class<RouterParamsError, Schema.TaggedStruct<"RouterParamsError", {
|
|
31
|
-
/** Which side of the match failed validation. */
|
|
31
|
+
/** Which side of the match failed validation. */
|
|
32
|
+
readonly source: Schema.Literals<readonly ["path", "query"]>;
|
|
33
|
+
/** The requested field names, for diagnostics. */
|
|
32
34
|
readonly keys: Schema.$Array<Schema.String>;
|
|
33
35
|
}>, import("effect/Cause").YieldableError>;
|
|
34
36
|
/**
|
|
35
37
|
* Tagged error raised by `Router.params` / `Router.query` when the live match does
|
|
36
|
-
* not satisfy the requested fields
|
|
38
|
+
* not satisfy the requested fields: either no route is matched, or a requested
|
|
37
39
|
* key is missing / fails its schema's `Type`-side validation. `source` records
|
|
38
40
|
* whether the failure was on the path params or the query, and `keys` lists the
|
|
39
41
|
* requested field names for diagnostics.
|
|
@@ -52,7 +54,8 @@ type RouteMatch = {
|
|
|
52
54
|
readonly _tag: "Matched";
|
|
53
55
|
readonly leaf: CompiledLeaf;
|
|
54
56
|
readonly path: Record<string, unknown>;
|
|
55
|
-
readonly query: Record<string, unknown>;
|
|
57
|
+
readonly query: Record<string, unknown>;
|
|
58
|
+
/** Normalized request URL (path + search), used by the outlet as a dedupe key. */
|
|
56
59
|
readonly url: string;
|
|
57
60
|
} | {
|
|
58
61
|
readonly _tag: "NotFound";
|
|
@@ -73,19 +76,16 @@ interface MatcherEntry {
|
|
|
73
76
|
}
|
|
74
77
|
/**
|
|
75
78
|
* Precompiles a {@link RouterDef} into ordered matcher entries (memoized per
|
|
76
|
-
* `RouterDef`).
|
|
77
|
-
* `def.httpApi` `"pages"` endpoints
|
|
78
|
-
* 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`
|
|
79
82
|
* by endpoint id. Matching stays local (SPA URL→leaf); see the refactor plan's
|
|
80
83
|
* _Feasibility constraint_.
|
|
81
84
|
*
|
|
82
85
|
* Entries are sorted most-specific first (fewer params, then longer pattern) so a
|
|
83
|
-
* static segment wins over a param segment at the same position (M6).
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* It resolves the common "static beats param at the same position" case, but two
|
|
87
|
-
* patterns with the same param count and length (e.g. `/a/:b/c` vs `/a/x/:d`)
|
|
88
|
-
* 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.
|
|
89
89
|
*/
|
|
90
90
|
declare function compileMatchers(def: RouterDef): readonly MatcherEntry[];
|
|
91
91
|
/**
|
|
@@ -98,7 +98,7 @@ declare function match(def: RouterDef, url: string): RouteMatch;
|
|
|
98
98
|
//#endregion
|
|
99
99
|
//#region src/router-service.d.ts
|
|
100
100
|
/**
|
|
101
|
-
* The universal router service. Provided per render
|
|
101
|
+
* The universal router service. Provided per render: by `RouterLive` on the
|
|
102
102
|
* client (History-API backed) and by a fixed per-request implementation on the
|
|
103
103
|
* server. Layouts and pages read it anywhere via `yield* Router`.
|
|
104
104
|
*
|
|
@@ -106,12 +106,12 @@ declare function match(def: RouterDef, url: string): RouteMatch;
|
|
|
106
106
|
* {@link Router.layout}, and {@link Router.router} build the route tree (mirroring
|
|
107
107
|
* `Component.gen` / `Boundary.catchTag` / `h.div`), and {@link Router.Outlet} /
|
|
108
108
|
* {@link Router.params} / {@link Router.query} deliver the outlet and the live
|
|
109
|
-
* 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:
|
|
110
110
|
* `yield* Router` reads the service; `Router.route(…)` authors a tree.
|
|
111
111
|
*/
|
|
112
112
|
/**
|
|
113
113
|
* The platform `HttpApiClient` derived from a router's `HttpApi` spine. Typed
|
|
114
|
-
* opaquely (`Client<any, …>`) because the spine is `HttpApi.Top
|
|
114
|
+
* opaquely (`Client<any, …>`) because the spine is `HttpApi.Top`: its
|
|
115
115
|
* group/endpoint shapes are assembled in a runtime loop by `buildHttpApi`, so a
|
|
116
116
|
* precise client type is not recoverable. Present (`Option.some`) on the client
|
|
117
117
|
* (`RouterLive`), absent (`Option.none`) on the server, which is itself the origin.
|
|
@@ -142,7 +142,8 @@ interface NavigateOptions {
|
|
|
142
142
|
readonly replace?: boolean;
|
|
143
143
|
}
|
|
144
144
|
declare const Router_base: Context.ServiceClass<Router, "@weftui/router/Router", {
|
|
145
|
-
/** The current match as a hot `Subscribable`; drives the outlet. */
|
|
145
|
+
/** The current match as a hot `Subscribable`; drives the outlet. */
|
|
146
|
+
readonly currentMatch: Subscribable.Subscribable<RouteMatch>;
|
|
146
147
|
/**
|
|
147
148
|
* Navigates to `to` (a path, optionally with a query). On the client this
|
|
148
149
|
* pushes History state and re-renders the affected outlet; on the server it
|
|
@@ -152,8 +153,8 @@ declare const Router_base: Context.ServiceClass<Router, "@weftui/router/Router",
|
|
|
152
153
|
/**
|
|
153
154
|
* The derived {@link RouterHttpApiClient} for network work (route prefetch,
|
|
154
155
|
* foundation for future loaders/data). `Option.some` on the client,
|
|
155
|
-
* `Option.none` on the server. SPA URL→leaf resolution does **not** use this
|
|
156
|
-
*
|
|
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_).
|
|
157
158
|
*/
|
|
158
159
|
readonly httpApiClient: Option.Option<RouterHttpApiClient>;
|
|
159
160
|
/**
|
|
@@ -174,17 +175,17 @@ declare const OutletTag_base: Context.ServiceClass<OutletTag, "@weftui/router/Ou
|
|
|
174
175
|
* reads it with `yield* Router.Outlet`.
|
|
175
176
|
*
|
|
176
177
|
* Typed **opaque** as `Node<never, never>` so splicing `[outlet]` adds nothing to
|
|
177
|
-
* a layout's local channels
|
|
178
|
+
* a layout's local channels: the subtree's real channels are aggregated
|
|
178
179
|
* structurally by {@link makeLayout} / {@link makeRouter}, never inferred across
|
|
179
180
|
* this DI boundary. Re-exported on the namespace as `Router.Outlet`.
|
|
180
181
|
*/
|
|
181
182
|
declare class OutletTag extends OutletTag_base {}
|
|
182
183
|
/**
|
|
183
184
|
* Reads the live match's **path params** for the requested `fields`. Snapshot
|
|
184
|
-
* semantics
|
|
185
|
+
* semantics (reads `yield* Router`, then `currentMatch.get`), returning the
|
|
185
186
|
* already-decoded values **directly**: the matcher decoded them against the leaf's
|
|
186
|
-
* full path schema, so no re-validation is needed
|
|
187
|
-
* 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}
|
|
188
189
|
* (`source: "path"`) only when no route is matched. Re-exported as `Router.params`.
|
|
189
190
|
*/
|
|
190
191
|
declare function readParams<F extends Fields>(fields: F): Effect.Effect<FieldsType<F>, RouterParamsError, Router>;
|
|
@@ -248,7 +249,7 @@ type FieldsType<F extends Fields> = Schema.Struct.Type<F>;
|
|
|
248
249
|
* The router passes `{ path, query }` into the leaf slot at render time (the
|
|
249
250
|
* {@link makeRoute} props-form overload), so a page can read them directly as props
|
|
250
251
|
* instead of via the `Router.params` / `Router.query` dependency-injection
|
|
251
|
-
* accessors. Layouts and deeper nodes
|
|
252
|
+
* accessors. Layouts and deeper nodes keep DI, since they can't take handler args.
|
|
252
253
|
*/
|
|
253
254
|
interface RouteHandlerProps<Path extends Fields = {}, Query extends Fields = {}> {
|
|
254
255
|
/** The live match's decoded path params (`Type` side of the route's `path`). */
|
|
@@ -260,26 +261,26 @@ interface RouteHandlerProps<Path extends Fields = {}, Query extends Fields = {}>
|
|
|
260
261
|
* The shape of a route/layout `component` slot: a callable producing a {@link Node},
|
|
261
262
|
* invoked by the router at render time. It accepts both a plain zero-arg thunk
|
|
262
263
|
* (`() => h.div(…)`) and a {@link Component} produced by `Component.make` /
|
|
263
|
-
* `Component.gen` (a generic `(props, children?) => Node`). The `props: any` arm
|
|
264
|
-
* 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
|
|
265
266
|
* assignable; the router calls the slot with no arguments.
|
|
266
267
|
*/
|
|
267
268
|
type ComponentSlot<N extends Node<any, any> = Node<any, any>> = (props: any) => N;
|
|
268
269
|
/**
|
|
269
270
|
* The {@link Node} a {@link ComponentSlot} produces when the router invokes it with no
|
|
270
|
-
* props/children
|
|
271
|
+
* props/children. Used to recover the slot's `E`/`R` channels for the route tree.
|
|
271
272
|
*
|
|
272
273
|
* A plain zero-arg thunk is matched first (`() => infer N`): a required-props
|
|
273
274
|
* `Component` is *not* assignable to `() => unknown`, so it falls through to the
|
|
274
275
|
* `Component` arm, where the internal `E`/`R` type parameters are read directly. This
|
|
275
|
-
* two-step form is deliberate
|
|
276
|
+
* two-step form is deliberate: `ReturnType<S>` collapses a generic `Component`'s
|
|
276
277
|
* channels to `unknown` (they depend on the erased `GenP`/`GenC`), whereas extracting
|
|
277
278
|
* the `Component<…, E, R>` parameters preserves them. Caller prop/children channels are
|
|
278
279
|
* never relevant here because the router supplies neither.
|
|
279
280
|
*/
|
|
280
281
|
type SlotNode<S> = S extends (() => infer N) ? N : S extends Component.Component<any, any, infer E, infer R> ? Node<E, R> : never;
|
|
281
282
|
/**
|
|
282
|
-
* 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
|
|
283
284
|
* {@link ComponentSlot} (a `Component.gen` / `Component.make` component, or a plain
|
|
284
285
|
* `() => Node` thunk) that the router invokes at render time and that reads the live
|
|
285
286
|
* match's params via `Router.params` / `Router.query`. `Path`/`Query` drive matching
|
|
@@ -300,7 +301,7 @@ interface RouteNode<Path extends Fields = {}, Query extends Fields = {}, E = nev
|
|
|
300
301
|
}
|
|
301
302
|
/**
|
|
302
303
|
* A layout wrapping an outlet (the next level down) in the route tree. A layout is
|
|
303
|
-
* **purely UI nesting
|
|
304
|
+
* **purely UI nesting**: it owns **no path or segment**; all path structure lives
|
|
304
305
|
* on routes. Its `component` is a {@link ComponentSlot} that splices the injected
|
|
305
306
|
* outlet via `yield* Router.Outlet`; the router invokes it per render and discharges
|
|
306
307
|
* that `Outlet` requirement. A layout that needs a param reads it via `Router.params`.
|
|
@@ -315,7 +316,7 @@ interface LayoutNode<E = never, R = never> {
|
|
|
315
316
|
/**
|
|
316
317
|
* Phantom marker for this layout subtree's aggregate error channel (see
|
|
317
318
|
* {@link TreeE}). Covariant (stores `E` directly) so a fully-discharged layout
|
|
318
|
-
* (`LayoutNode<never, never
|
|
319
|
+
* (`LayoutNode<never, never>`, its `Outlet` provided and no subtree errors) stays
|
|
319
320
|
* assignable to the `LayoutNode<any, any>` arm of {@link TreeNode}.
|
|
320
321
|
*/
|
|
321
322
|
readonly _E?: E;
|
|
@@ -333,26 +334,17 @@ type SubtreeE<C extends readonly TreeNode[]> = TreeE<C[number]>;
|
|
|
333
334
|
/** Aggregate requirement channel over a children tuple (distributes over `C[number]`). */
|
|
334
335
|
type SubtreeR<C extends readonly TreeNode[]> = TreeR<C[number]>;
|
|
335
336
|
/**
|
|
336
|
-
* Declares a leaf page. The `component` *is* the route handler
|
|
337
|
+
* Declares a leaf page. The `component` *is* the route handler: a thunk the
|
|
337
338
|
* router invokes at render time; its error / requirement channels propagate up the
|
|
338
339
|
* tree. Two authoring forms are accepted:
|
|
339
340
|
*
|
|
340
|
-
* - **Handler-arg props
|
|
341
|
+
* - **Handler-arg props**: the slot declares `(props: {@link RouteHandlerProps})`
|
|
341
342
|
* and the router passes the live match's decoded `{ path, query }` in directly
|
|
342
343
|
* (first overload; `path`/`query` are inferred from the route's `path`/`query`
|
|
343
|
-
* fields). A plain zero-arg thunk works too
|
|
344
|
-
* - **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
|
|
345
346
|
* reads the live match via `Router.params` / `Router.query` (second overload).
|
|
346
347
|
*
|
|
347
|
-
* @example Handler-arg props (decoded `{ path, query }`)
|
|
348
|
-
* ```ts
|
|
349
|
-
* Router.route("users/:id", {
|
|
350
|
-
* path: { id: Schema.NumberFromString },
|
|
351
|
-
* query: { tab: Schema.optional(Schema.String) },
|
|
352
|
-
* component: ({ path, query }) => h.div({}, `User ${path.id} (${query.tab ?? "info"})`),
|
|
353
|
-
* });
|
|
354
|
-
* ```
|
|
355
|
-
*
|
|
356
348
|
* @example Dependency injection (`Router.params` / a `Component`)
|
|
357
349
|
* ```ts
|
|
358
350
|
* Router.route("about", { component: Component.make(() => h.h1({}, "About")) });
|
|
@@ -400,24 +392,16 @@ declare function makeLayout<C extends readonly TreeNode[], S extends ComponentSl
|
|
|
400
392
|
}, children: C): LayoutNode<Node.Error<SlotNode<S>> | SubtreeE<C>, Exclude<Node.Context<SlotNode<S>>, Router.Outlet> | SubtreeR<C>>;
|
|
401
393
|
/**
|
|
402
394
|
* Wraps a dynamic-import `load` as a lazy {@link ComponentSlot}: the route's descriptor
|
|
403
|
-
* (`segment`, `path`/`query`) stays eager and matchable, while the component
|
|
404
|
-
* 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
|
|
405
397
|
* the returned slot at render time; it awaits `load` then renders the resolved component,
|
|
406
398
|
* adopting the server DOM in place on hydration (flash-free) and fetching the chunk on
|
|
407
399
|
* client navigation. Exposed as {@link Router.lazy}. See `lazy-component.specs.md`.
|
|
408
400
|
*
|
|
409
401
|
* The resolved value is a component slot (`Component.gen` / `Component.make`, or a
|
|
410
|
-
* `() => Node` thunk)
|
|
402
|
+
* `() => Node` thunk), the shape `component:` already accepts, so its `E`/`R` channels
|
|
411
403
|
* are recovered via {@link SlotNode} and propagate up the tree exactly as an eager
|
|
412
404
|
* component's do.
|
|
413
|
-
*
|
|
414
|
-
* @example
|
|
415
|
-
* ```ts
|
|
416
|
-
* Router.route("docs/:category/:slug", {
|
|
417
|
-
* path: { category: Schema.String, slug: Schema.String },
|
|
418
|
-
* component: Router.lazy(() => import("./doc-page").then((m) => m.DocPage)),
|
|
419
|
-
* });
|
|
420
|
-
* ```
|
|
421
405
|
*/
|
|
422
406
|
declare function lazyComponent<S extends ComponentSlot>(load: () => Promise<S>): () => Node<Node.Error<SlotNode<S>>, Node.Context<SlotNode<S>>>;
|
|
423
407
|
//#endregion
|
|
@@ -426,7 +410,7 @@ declare function lazyComponent<S extends ComponentSlot>(load: () => Promise<S>):
|
|
|
426
410
|
* A compiled layout level: its component slot plus the dedupe `patternPrefix` used
|
|
427
411
|
* by the client outlet to key the level. A layout owns no path of its own, so the
|
|
428
412
|
* prefix is derived as the **longest common path-segment prefix of every leaf in
|
|
429
|
-
* the layout's subtree
|
|
413
|
+
* the layout's subtree**: it changes (and the level re-renders) exactly when a
|
|
430
414
|
* param shared by all those leaves changes, and persists otherwise.
|
|
431
415
|
*/
|
|
432
416
|
interface CompiledLayout {
|
|
@@ -452,7 +436,7 @@ interface CompiledLeaf {
|
|
|
452
436
|
/**
|
|
453
437
|
* Path-param schema. Its **encoded** side is typed string-encodeable
|
|
454
438
|
* (`Record<string, string | undefined>`) so it satisfies platform's
|
|
455
|
-
* `HttpApiEndpoint` `params` constraint without an `as any` cast
|
|
439
|
+
* `HttpApiEndpoint` `params` constraint without an `as any` cast: param schemas
|
|
456
440
|
* round-trip strings, so the `Schema.Struct` value is asserted to this shape.
|
|
457
441
|
*/
|
|
458
442
|
readonly pathSchema: Schema.Codec<Record<string, unknown>, Readonly<Record<string, string | undefined>>>;
|
|
@@ -516,9 +500,9 @@ declare const leafRegistry: WeakMap<RouteNode<any, any, any, any>, CompiledLeaf>
|
|
|
516
500
|
* Pass 1 walks the tree: only **routes** contribute path parts (layouts own no
|
|
517
501
|
* path), so each leaf's `parts` come solely from the route segments on its branch,
|
|
518
502
|
* and its ancestor `LayoutNode`s are recorded in order. Pass 2 derives one shared
|
|
519
|
-
* {@link CompiledLayout} per distinct layout node
|
|
520
|
-
* longest common path prefix of that layout's subtree leaves
|
|
521
|
-
* 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.
|
|
522
506
|
*/
|
|
523
507
|
declare function compile(def: {
|
|
524
508
|
root: TreeNode;
|
|
@@ -526,7 +510,7 @@ declare function compile(def: {
|
|
|
526
510
|
}): Compiled;
|
|
527
511
|
/**
|
|
528
512
|
* Builds the authoritative `HttpApi` for a compiled tree (S4): a single `"pages"`
|
|
529
|
-
* group
|
|
513
|
+
* group holding one GET endpoint per leaf, at each leaf's full path
|
|
530
514
|
* pattern, carrying `params: pathSchema`, `query: querySchema`, a
|
|
531
515
|
* `Schema.String` (text/HTML) success, and a `RouterNotFound → 404` error. The tree
|
|
532
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-BgJmsO_g.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-Bb7AknG_.js";
|
|
2
|
+
import { i as href, n as outletNode, r as HrefArgs, t as RouterApp } from "./outlet-2AnUWKQD.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-BgJmsO_g.js";
|
|
2
|
+
import { n as compileMatchers, r as match, t as href } from "./href-3swSSbn_.js";
|
|
3
3
|
export { Router, RouterApp, RouterNotFound, RouterParamsError, buildHttpApi, compile, compileMatchers, href, isRouterNotFound, leafRegistry, match, notFound, outletNode };
|
|
@@ -1,6 +1,5 @@
|
|
|
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-Bb7AknG_.js";
|
|
2
2
|
import { Node } from "@weftui/core";
|
|
3
|
-
|
|
4
3
|
//#region src/href.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* The `href` argument object for a leaf with path fields `Path` and query fields
|