@weftui/router 0.0.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 Stef van Wijchen
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.
@@ -0,0 +1,109 @@
1
+ import { b as NavigateOptions, i as RouterDef, m as RouteNode, u as Fields, x as Router } from "../compile-C0JShTTR.js";
2
+ import { n as outletNode, r as HrefArgs, t as RouterApp } from "../outlet-BBEjKu3z.js";
3
+ import { Effect, Layer, Scope } from "effect";
4
+ import { AppRpcClientTag } from "@weftui/core";
5
+ import { RpcGroup } from "@effect/rpc";
6
+
7
+ //#region src/client/router-live.d.ts
8
+ /** Options for {@link RouterLive}. */
9
+ interface RouterLiveOptions {
10
+ /**
11
+ * Base URL for the derived `HttpApiClient` (route prefetch) and the rpc client's
12
+ * `POST /_eui/rpc` endpoint. Defaults to the document's same origin
13
+ * (`window.location.origin`).
14
+ */
15
+ readonly baseUrl?: string | URL;
16
+ /**
17
+ * The app's `Boundary.rpc` foundation: the merged `RpcGroup` contract (shared
18
+ * with the server handler Layer). Backs the {@link AppRpcClientTag} seam so a
19
+ * hydrated boundary refetch — and a client-first SPA mount — resolve over the
20
+ * network rpc client.
21
+ */
22
+ readonly rpc: {
23
+ /** The app's merged `RpcGroup` (pure Schema contract). */readonly group: RpcGroup.RpcGroup<any>;
24
+ };
25
+ }
26
+ /**
27
+ * The client `Router` layer, backed by the History API. Seeds a
28
+ * `SubscriptionRef` from `window.location`, listens for `popstate`, and exposes
29
+ * `currentMatch` as the ref mapped through the shared matcher. `navigate` pushes
30
+ * History state and updates the ref. Also installs the same-origin link click
31
+ * interceptor for the layer's lifetime.
32
+ *
33
+ * It additionally derives a real {@link RouterHttpApiClient} from `def.httpApi`
34
+ * (over `FetchHttpClient`, `baseUrl` default same-origin) and exposes it on the
35
+ * `Router` service for network work. SPA URL→leaf resolution stays local via the
36
+ * shared {@link match}er — both sides read the one `def.httpApi` definition.
37
+ *
38
+ * Alongside `Router` it provides the core {@link AppRpcClientTag} seam — a
39
+ * **network** flat rpc client (`RpcClient.make` over `layerProtocolHttp` →
40
+ * `POST /_eui/rpc`) — so `@weftui/dom` can resolve a `Boundary.rpc` (hydrated
41
+ * refetch and client-first mount) without depending on this package or
42
+ * `@effect/rpc`.
43
+ */
44
+ declare function RouterLive(def: RouterDef, options: RouterLiveOptions): Layer.Layer<Router | AppRpcClientTag>;
45
+ //#endregion
46
+ //#region src/client/link.d.ts
47
+ /**
48
+ * Installs a global, delegated click interceptor (in the `Router` layer scope)
49
+ * that turns plain same-origin `h.a({ href })` clicks into SPA navigation when
50
+ * the href resolves to a route (L1). Modified clicks, non-left buttons,
51
+ * `target=_blank`, `download`, external origins, same-document (hash-only or
52
+ * identical-URL) navigations, and non-matching hrefs fall through to the
53
+ * browser's native handling — the interceptor leaves `preventDefault` untouched
54
+ * in those cases (L2). The listener is removed on scope teardown (L3).
55
+ *
56
+ * @param def - The router definition, used to decide whether an href matches a route.
57
+ * @param navigate - The router's `navigate`, run via the captured runtime on a match.
58
+ */
59
+ declare function installLinkInterceptor(def: RouterDef, navigate: (to: string) => Effect.Effect<void>): Effect.Effect<void, never, Scope.Scope>;
60
+ //#endregion
61
+ //#region src/client/navigation.d.ts
62
+ /**
63
+ * Programmatic, type-safe navigation helpers built on the `Router` service and the
64
+ * type-safe {@link href} builder. They mirror the History API the client `Router`
65
+ * layer (`RouterLive`) is backed by:
66
+ *
67
+ * - {@link navigate} — go to a leaf route reference with typed `{ path, query }`.
68
+ * - {@link push} / {@link replace} — go to a raw `path + search` string.
69
+ * - {@link back} / {@link forward} — step through History (`history.go`).
70
+ * - {@link setQuery} / {@link patchQuery} — change the current route's query in
71
+ * place, re-encoding through the matched leaf's `querySchema`.
72
+ *
73
+ * All but `back`/`forward` require the `Router` service (run them within the layer
74
+ * provided by `RouterLive`); `back`/`forward` only touch `window.history`.
75
+ */
76
+ /**
77
+ * Navigates to a leaf route `ref` with typed `path`/`query` args, building the URL
78
+ * via {@link href} (so it round-trips with `match`) and pushing — or, with
79
+ * `options.replace`, replacing — the History entry. `path` is required when the
80
+ * route has path params; `query` is optional when every query field is optional
81
+ * (same requiredness rules as `href`).
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * yield* navigate(userRoute, { path: { id: 42 } });
86
+ * yield* navigate(userRoute, { path: { id: 42 }, query: { tab: "posts" } }, { replace: true });
87
+ * ```
88
+ */
89
+ declare function navigate<Path extends Fields, Query extends Fields>(ref: RouteNode<Path, Query, any, any>, ...args: {} extends HrefArgs<Path, Query> ? [args?: HrefArgs<Path, Query>, options?: NavigateOptions] : [args: HrefArgs<Path, Query>, options?: NavigateOptions]): Effect.Effect<void, never, Router>;
90
+ /** Navigates to a raw `path + search` string, pushing a new History entry. */
91
+ declare const push: (to: string) => Effect.Effect<void, never, Router>;
92
+ /** Navigates to a raw `path + search` string, replacing the current History entry. */
93
+ declare const replace: (to: string) => Effect.Effect<void, never, Router>;
94
+ /** Steps one entry back in History (`history.go(-1)`); the `popstate` handler resyncs. */
95
+ declare const back: () => Effect.Effect<void>;
96
+ /** Steps one entry forward in History (`history.go(1)`); the `popstate` handler resyncs. */
97
+ declare const forward: () => Effect.Effect<void>;
98
+ /**
99
+ * Replaces the current route's query entirely with `query` (re-encoded through the
100
+ * matched leaf's `querySchema`), keeping the path. Pass `{}` to clear the query.
101
+ */
102
+ declare const setQuery: (query: Record<string, unknown>, options?: NavigateOptions) => Effect.Effect<void, never, Router>;
103
+ /**
104
+ * Merges `partial` into the current route's decoded query (re-encoded through the
105
+ * matched leaf's `querySchema`), keeping the path and any unspecified query fields.
106
+ */
107
+ declare const patchQuery: (partial: Record<string, unknown>, options?: NavigateOptions) => Effect.Effect<void, never, Router>;
108
+ //#endregion
109
+ export { Router, RouterApp, RouterLive, outletNode as RouterOutlet, outletNode, back, forward, installLinkInterceptor, navigate, patchQuery, push, replace, setQuery };
@@ -0,0 +1 @@
1
+ import{n as e,r as t,t as n}from"../outlet-C7p8KXmO.js";import{r,t as i}from"../href-Dl30XcF4.js";import{FetchHttpClient as a,HttpApiClient as o}from"@effect/platform";import{Context as s,Effect as c,Layer as l,Option as u,Runtime as d,Schema as f,Stream as p,Subscribable as m,SubscriptionRef as h}from"effect";import{AppRpcClientTag as g}from"@weftui/core";import{RpcClient as _,RpcSerialization as v}from"@effect/rpc";function y(e,t){return c.gen(function*(){let n=yield*c.runtime(),i=i=>{if(i.defaultPrevented||i.button!==0||i.metaKey||i.ctrlKey||i.shiftKey||i.altKey)return;let a=i.target,o=a instanceof Element?a.closest(`a`):null;if(o===null)return;let s=o.getAttribute(`target`);if(o.hasAttribute(`download`)||s!==null&&s!==`_self`||o.getAttribute(`rel`)===`external`)return;let c=o.getAttribute(`href`);if(c===null||c.length===0)return;let l;try{l=new URL(c,window.location.href)}catch{return}if(l.origin!==window.location.origin)return;let u=`${l.pathname}${l.search}`;u!==`${window.location.pathname}${window.location.search}`&&r(e,u)._tag===`Matched`&&(i.preventDefault(),d.runFork(n)(t(u)))};yield*c.acquireRelease(c.sync(()=>document.addEventListener(`click`,i)),()=>c.sync(()=>document.removeEventListener(`click`,i)))})}function b(){return`${window.location.pathname}${window.location.search}`}function x(e){let t=new URL(e,window.location.href);return`${t.pathname}${t.search}`}function S(e,n){return l.scopedContext(c.gen(function*(){let i=yield*h.make(b()),f=yield*c.runtime(),S=yield*o.make(e.httpApi,{baseUrl:n.baseUrl??window.location.origin}).pipe(c.provide(a.layer)),C=()=>{d.runFork(f)(h.set(i,b()))};yield*c.acquireRelease(c.sync(()=>window.addEventListener(`popstate`,C)),()=>c.sync(()=>window.removeEventListener(`popstate`,C)));let w=(e,t)=>c.gen(function*(){let n=x(e);yield*c.sync(()=>{t?.replace===!0?window.history.replaceState(null,``,n):window.history.pushState(null,``,n)}),yield*h.set(i,n)});yield*y(e,w);let T=m.make({get:c.map(h.get(i),t=>r(e,t)),changes:p.map(i.changes,t=>r(e,t))}),E=String(n.baseUrl??window.location.origin).replace(/\/$/,``),D=yield*_.make(n.rpc.group,{flatten:!0}).pipe(c.provide(_.layerProtocolHttp({url:`${E}/_eui/rpc`}).pipe(l.provide(l.mergeAll(a.layer,v.layerJson))))),O=g.of({call:(e,t)=>D(e,t)}),k=t.of({currentMatch:T,navigate:w,httpApiClient:u.some(S)});return s.make(t,k).pipe(s.add(g,O))}))}function C(e,...n){let[r,a]=n,o=i(e,r);return c.flatMap(t,e=>e.navigate(o,a))}const w=e=>c.flatMap(t,t=>t.navigate(e)),T=e=>c.flatMap(t,t=>t.navigate(e,{replace:!0})),E=()=>c.sync(()=>window.history.go(-1)),D=()=>c.sync(()=>window.history.go(1));function O(e){let t=new URLSearchParams;for(let n of Object.keys(e).sort()){let r=e[n];r!=null&&t.append(n,String(r))}return t.toString()}function k(e){let t=e.indexOf(`?`);return t===-1?e:e.slice(0,t)}function A(e,n){return c.gen(function*(){let r=yield*t,i=yield*r.currentMatch.get;if(i._tag!==`Matched`)return;let a=e(i.query),o=O(f.encodeUnknownSync(i.leaf.querySchema)(a)),s=k(i.url);yield*r.navigate(o.length>0?`${s}?${o}`:s,n)})}const j=(e,t)=>A(()=>e,t),M=(e,t)=>A(t=>({...t,...e}),t);export{t as Router,n as RouterApp,S as RouterLive,e as RouterOutlet,e as outletNode,E as back,D as forward,y as installLinkInterceptor,C as navigate,M as patchQuery,w as push,T as replace,j as setQuery};
@@ -0,0 +1,508 @@
1
+ import { HttpApi, HttpApiClient } from "@effect/platform";
2
+ import { Context, Effect, Option, Schema, Subscribable } from "effect";
3
+ import { Component, Node } from "@weftui/core";
4
+
5
+ //#region src/errors.d.ts
6
+ declare const RouterNotFound_base: Schema.TaggedErrorClass<RouterNotFound, "RouterNotFound", {
7
+ readonly _tag: Schema.tag<"RouterNotFound">;
8
+ } & {
9
+ /** The path that could not be resolved, when known. */path: Schema.optional<typeof Schema.String>;
10
+ }>;
11
+ /**
12
+ * Tagged error raised by {@link notFound} and caught by the router's internal
13
+ * not-found boundary. Exported so a user can place their own
14
+ * `Boundary.catchTag("RouterNotFound", …)` to override the fallback for a subtree
15
+ * (the router's internal boundary is outermost, so a nearer user boundary wins).
16
+ *
17
+ * Modeled as a `Schema.TaggedError` so it can be encoded/decoded across the wire
18
+ * the same way `Boundary.rpc` replays typed failures.
19
+ */
20
+ declare class RouterNotFound extends RouterNotFound_base {}
21
+ /**
22
+ * Short-circuits the current page render with a {@link RouterNotFound} failure,
23
+ * Next.js-style. Callable from any page or layout `component`; the nearest
24
+ * enclosing not-found boundary (the router's internal one by default) renders the
25
+ * configured `notFound` page in its place. The server responds with HTTP 404.
26
+ *
27
+ * @param path - Optional path to attach for diagnostics.
28
+ */
29
+ declare const notFound: (path?: string) => Effect.Effect<never, RouterNotFound>;
30
+ /** Type guard recognising a {@link RouterNotFound} value regardless of its prototype. */
31
+ declare const isRouterNotFound: (u: unknown) => u is RouterNotFound;
32
+ declare const RouterParamsError_base: Schema.TaggedErrorClass<RouterParamsError, "RouterParamsError", {
33
+ readonly _tag: Schema.tag<"RouterParamsError">;
34
+ } & {
35
+ /** Which side of the match failed validation. */source: Schema.Literal<["path", "query"]>; /** The requested field names, for diagnostics. */
36
+ keys: Schema.Array$<typeof Schema.String>;
37
+ }>;
38
+ /**
39
+ * Tagged error raised by `Router.params` / `Router.query` when the live match does
40
+ * not satisfy the requested fields — either no route is matched, or a requested
41
+ * key is missing / fails its schema's `Type`-side validation. `source` records
42
+ * whether the failure was on the path params or the query, and `keys` lists the
43
+ * requested field names for diagnostics.
44
+ *
45
+ * It bubbles up through the route tree's aggregate error channel, so a user may
46
+ * place a `Boundary.catchTag("RouterParamsError", …)` to recover within a subtree.
47
+ *
48
+ * Modeled as a `Schema.TaggedError` so it can be encoded/decoded across the wire
49
+ * the same way `RouterNotFound` and `Boundary.rpc` replay typed failures.
50
+ */
51
+ declare class RouterParamsError extends RouterParamsError_base {}
52
+ //#endregion
53
+ //#region src/matcher.d.ts
54
+ /** The resolved match for a URL: a leaf with decoded params/query, or not-found. */
55
+ type RouteMatch = {
56
+ readonly _tag: "Matched";
57
+ readonly leaf: CompiledLeaf;
58
+ readonly path: Record<string, unknown>;
59
+ readonly query: Record<string, unknown>; /** Normalized request URL (path + search), used by the outlet as a dedupe key. */
60
+ readonly url: string;
61
+ } | {
62
+ readonly _tag: "NotFound";
63
+ readonly url: string;
64
+ };
65
+ /** A string-encodeable schema as carried by an HttpApi endpoint's path/urlParams slot. */
66
+ type ParamSchema = Schema.Schema<Record<string, unknown>, unknown, never>;
67
+ /** A precompiled regex + decode schemas for one leaf, sourced from its HttpApi endpoint. */
68
+ interface MatcherEntry {
69
+ /** The compiled leaf (render/nesting metadata), resolved from the endpoint id. */
70
+ readonly leaf: CompiledLeaf;
71
+ readonly regex: RegExp;
72
+ readonly paramNames: readonly string[];
73
+ /** Path-param schema, read from the endpoint's `setPath` slot. */
74
+ readonly pathSchema: ParamSchema;
75
+ /** Query schema, read from the endpoint's `setUrlParams` slot. */
76
+ readonly querySchema: ParamSchema;
77
+ }
78
+ /**
79
+ * Precompiles a {@link RouterDef} into ordered matcher entries (memoized per
80
+ * `RouterDef`). The patterns and path/query schemas are read from the authoritative
81
+ * `def.httpApi` `"pages"` endpoints — the single source of truth the server dispatch
82
+ * also reads — and each entry's render metadata leaf is resolved from `def.compiled`
83
+ * by endpoint id. Matching stays local (SPA URL→leaf); see the refactor plan's
84
+ * _Feasibility constraint_.
85
+ *
86
+ * Entries are sorted most-specific first (fewer params, then longer pattern) so a
87
+ * static segment wins over a param segment at the same position (M6).
88
+ *
89
+ * Note: the specificity order is a global heuristic (param count, then length).
90
+ * It resolves the common "static beats param at the same position" case, but two
91
+ * patterns with the same param count and length (e.g. `/a/:b/c` vs `/a/x/:d`)
92
+ * fall back to endpoint order.
93
+ */
94
+ declare function compileMatchers(def: RouterDef): readonly MatcherEntry[];
95
+ /**
96
+ * Matches a request URL against a {@link RouterDef} (M1–M7). Returns the decoded
97
+ * `Matched` leaf, or `NotFound` when nothing matches or a path/query decode fails
98
+ * (decode failure is treated as no-match, not an error). Patterns and schemas come
99
+ * from `def.httpApi` via {@link compileMatchers}.
100
+ */
101
+ declare function match(def: RouterDef, url: string): RouteMatch;
102
+ //#endregion
103
+ //#region src/router-service.d.ts
104
+ /**
105
+ * The universal router service. Provided per render — by `RouterLive` on the
106
+ * client (History-API backed) and by a fixed per-request implementation on the
107
+ * server. Layouts and pages read it anywhere via `yield* Router`.
108
+ *
109
+ * The `Router` symbol is also the authoring namespace: {@link Router.route},
110
+ * {@link Router.layout}, and {@link Router.router} build the route tree (mirroring
111
+ * `Component.gen` / `Boundary.catchTag` / `h.div`), and {@link Router.Outlet} /
112
+ * {@link Router.params} / {@link Router.query} deliver the outlet and the live
113
+ * match's params/query by dependency injection. The roles merge by declaration —
114
+ * `yield* Router` reads the service; `Router.route(…)` authors a tree.
115
+ */
116
+ /**
117
+ * The platform `HttpApiClient` derived from a router's `HttpApi` spine. Typed
118
+ * opaquely (`Client<any, …>`) because the spine is `HttpApi.Any` — its
119
+ * group/endpoint shapes are assembled in a runtime loop by `buildHttpApi`, so a
120
+ * precise client type is not recoverable. Present (`Option.some`) on the client
121
+ * (`RouterLive`), absent (`Option.none`) on the server, which is itself the origin.
122
+ */
123
+ type RouterHttpApiClient = HttpApiClient.Client<any, any, never>;
124
+ /** Options for a router {@link Router} `navigate` call. */
125
+ interface NavigateOptions {
126
+ /**
127
+ * Replace the current History entry (`history.replaceState`) instead of pushing
128
+ * a new one (`history.pushState`). Defaults to `false` (push). A no-op on the
129
+ * server, where navigation is a client concern.
130
+ */
131
+ readonly replace?: boolean;
132
+ }
133
+ declare const Router_base: Context.TagClass<Router, "@weftui/router/Router", {
134
+ /** The current match as a hot `Subscribable`; drives the outlet. */readonly currentMatch: Subscribable.Subscribable<RouteMatch>;
135
+ /**
136
+ * Navigates to `to` (a path, optionally with a query). On the client this
137
+ * pushes History state and re-renders the affected outlet; on the server it
138
+ * is a no-op (navigation is a client concern).
139
+ */
140
+ readonly navigate: (to: string, options?: NavigateOptions) => Effect.Effect<void>;
141
+ /**
142
+ * The derived {@link RouterHttpApiClient} for network work (route prefetch,
143
+ * foundation for future loaders/data). `Option.some` on the client,
144
+ * `Option.none` on the server. SPA URL→leaf resolution does **not** use this —
145
+ * it stays local via the shared matcher (see the refactor _Feasibility constraint_).
146
+ */
147
+ readonly httpApiClient: Option.Option<RouterHttpApiClient>;
148
+ }>;
149
+ declare class Router extends Router_base {}
150
+ declare const OutletTag_base: Context.TagClass<OutletTag, "@weftui/router/Outlet", Node<never, never>>;
151
+ /**
152
+ * The injected outlet: the node a layout (or the server document shell) splices
153
+ * to place the next level down. Provided per render by the router
154
+ * (`Effect.provideService(layout.component({}), OutletTag, innerNode)`); a layout
155
+ * reads it with `yield* Router.Outlet`.
156
+ *
157
+ * Typed **opaque** as `Node<never, never>` so splicing `[outlet]` adds nothing to
158
+ * a layout's local channels — the subtree's real channels are aggregated
159
+ * structurally by {@link makeLayout} / {@link makeRouter}, never inferred across
160
+ * this DI boundary. Re-exported on the namespace as `Router.Outlet`.
161
+ */
162
+ declare class OutletTag extends OutletTag_base {}
163
+ /**
164
+ * Reads the live match's **path params** for the requested `fields`. Snapshot
165
+ * semantics — reads `yield* Router` then `currentMatch.get` — and returns the
166
+ * already-decoded values **directly**: the matcher decoded them against the leaf's
167
+ * full path schema, so no re-validation is needed (the cast is sound — the picked
168
+ * subset is the `Type` side of `fields`). Fails with a {@link RouterParamsError}
169
+ * (`source: "path"`) only when no route is matched. Re-exported as `Router.params`.
170
+ */
171
+ declare function readParams<F extends Fields>(fields: F): Effect.Effect<FieldsType<F>, RouterParamsError, Router>;
172
+ /**
173
+ * Reads the live match's **query** for the requested `fields`. Same snapshot +
174
+ * direct-read semantics as {@link readParams}, failing with a
175
+ * {@link RouterParamsError} (`source: "query"`) only on a no-match. Re-exported as
176
+ * `Router.query`.
177
+ */
178
+ declare function readQuery<F extends Fields>(fields: F): Effect.Effect<FieldsType<F>, RouterParamsError, Router>;
179
+ /**
180
+ * Reactive counterpart to {@link readParams}: a {@link Subscribable} of the live
181
+ * match's **path params** for `fields`, derived from `currentMatch.changes`. It
182
+ * re-emits on every navigation and stays live across `NotFound` (yielding the empty
183
+ * subset), so a component can render `[(yield* Router.paramsStream(fields)).changes]`
184
+ * and update in place even when the outlet keeps the same leaf mounted. Re-exported
185
+ * as `Router.paramsStream`.
186
+ */
187
+ declare function subscribeParams<F extends Fields>(fields: F): Effect.Effect<Subscribable.Subscribable<FieldsType<F>>, never, Router>;
188
+ /**
189
+ * Reactive counterpart to {@link readQuery}: a {@link Subscribable} of the live
190
+ * match's **query** for `fields`. Especially useful for query-only changes
191
+ * (`setQuery` / `patchQuery`), which keep the same leaf mounted: a snapshot
192
+ * `Router.query` would not update, but this stream does. Re-exported as
193
+ * `Router.queryStream`.
194
+ */
195
+ declare function subscribeQuery<F extends Fields>(fields: F): Effect.Effect<Subscribable.Subscribable<FieldsType<F>>, never, Router>;
196
+ declare namespace Router {
197
+ /** Declares a leaf page. See {@link makeRoute}. */
198
+ const route: typeof makeRoute;
199
+ /** Declares a layout wrapping an injected outlet. See {@link makeLayout}. */
200
+ const layout: typeof makeLayout;
201
+ /** Seals a route tree into a `RouterDef`. See {@link makeRouter}. */
202
+ const router: typeof makeRouter;
203
+ /** The injected outlet service value (yieldable Tag). See {@link OutletTag}. */
204
+ const Outlet: typeof OutletTag;
205
+ /** The injected outlet service identity (for `Exclude<R, Router.Outlet>`). */
206
+ type Outlet = OutletTag;
207
+ /** Reads the live match's path params for the requested fields. See {@link readParams}. */
208
+ const params: typeof readParams;
209
+ /** Reads the live match's query for the requested fields. See {@link readQuery}. */
210
+ const query: typeof readQuery;
211
+ /** Reactive {@link Subscribable} of the live match's path params. See {@link subscribeParams}. */
212
+ const paramsStream: typeof subscribeParams;
213
+ /** Reactive {@link Subscribable} of the live match's query. See {@link subscribeQuery}. */
214
+ const queryStream: typeof subscribeQuery;
215
+ }
216
+ //#endregion
217
+ //#region src/route-tree.d.ts
218
+ /** Record of field name → `Schema` used for path-param and query schemas. */
219
+ type Fields = Schema.Struct.Fields;
220
+ /** Decoded value of a {@link Fields} record (the `Type` side of its `Schema.Struct`). */
221
+ type FieldsType<F extends Fields> = Schema.Struct.Type<F>;
222
+ /**
223
+ * The handler-arg props a leaf `component` may declare: the live match's decoded
224
+ * path params and query, derived from the route's `path` / `query` {@link Fields}.
225
+ * The router passes `{ path, query }` into the leaf slot at render time (the
226
+ * {@link makeRoute} props-form overload), so a page can read them directly as props
227
+ * instead of via the `Router.params` / `Router.query` dependency-injection
228
+ * accessors. Layouts and deeper nodes — which can't take handler args — keep DI.
229
+ */
230
+ interface RouteHandlerProps<Path extends Fields = {}, Query extends Fields = {}> {
231
+ /** The live match's decoded path params (`Type` side of the route's `path`). */
232
+ readonly path: FieldsType<Path>;
233
+ /** The live match's decoded query (`Type` side of the route's `query`). */
234
+ readonly query: FieldsType<Query>;
235
+ }
236
+ /**
237
+ * The shape of a route/layout `component` slot: a callable producing a {@link Node},
238
+ * invoked by the router at render time. It accepts both a plain zero-arg thunk
239
+ * (`() => h.div(…)`) and a {@link Component} produced by `Component.make` /
240
+ * `Component.gen` (a generic `(props, children?) => Node`). The `props: any` arm —
241
+ * rather than `()` — is what keeps a required-props `Component<…>` structurally
242
+ * assignable; the router calls the slot with no arguments.
243
+ */
244
+ type ComponentSlot<N extends Node<any, any> = Node<any, any>> = (props: any) => N;
245
+ /**
246
+ * The {@link Node} a {@link ComponentSlot} produces when the router invokes it with no
247
+ * props/children — used to recover the slot's `E`/`R` channels for the route tree.
248
+ *
249
+ * A plain zero-arg thunk is matched first (`() => infer N`): a required-props
250
+ * `Component` is *not* assignable to `() => unknown`, so it falls through to the
251
+ * `Component` arm, where the internal `E`/`R` type parameters are read directly. This
252
+ * two-step form is deliberate — `ReturnType<S>` collapses a generic `Component`'s
253
+ * channels to `unknown` (they depend on the erased `GenP`/`GenC`), whereas extracting
254
+ * the `Component<…, E, R>` parameters preserves them. Caller prop/children channels are
255
+ * never relevant here because the router supplies neither.
256
+ */
257
+ type SlotNode<S> = S extends (() => infer N) ? N : S extends Component.Component<any, any, infer E, infer R> ? Node<E, R> : never;
258
+ /**
259
+ * A leaf page in the route tree. Its `component` *is* its handler — a
260
+ * {@link ComponentSlot} (a `Component.gen` / `Component.make` component, or a plain
261
+ * `() => Node` thunk) that the router invokes at render time and that reads the live
262
+ * match's params via `Router.params` / `Router.query`. `Path`/`Query` drive matching
263
+ * and `href`; `E`/`R` are phantom markers carrying the node's channels (recovered via
264
+ * {@link SlotNode}) so they propagate up the tree. The callable slot defers
265
+ * construction (so `href(…)` runs after compile) and mirrors the `notFound` slot.
266
+ */
267
+ interface RouteNode<Path extends Fields = {}, Query extends Fields = {}, E = never, R = never> {
268
+ readonly _tag: "Route";
269
+ readonly segment: string;
270
+ readonly path: Path;
271
+ readonly query: Query;
272
+ readonly component: ComponentSlot;
273
+ /** Phantom marker for this leaf's error channel (see {@link TreeE}). */
274
+ readonly _E?: E;
275
+ /** Phantom marker for this leaf's requirement channel (see {@link TreeR}). */
276
+ readonly _R?: R;
277
+ }
278
+ /**
279
+ * A layout wrapping an outlet (the next level down) in the route tree. A layout is
280
+ * **purely UI nesting** — it owns **no path or segment**; all path structure lives
281
+ * on routes. Its `component` is a {@link ComponentSlot} that splices the injected
282
+ * outlet via `yield* Router.Outlet`; the router invokes it per render and discharges
283
+ * that `Outlet` requirement. A layout that needs a param reads it via `Router.params`.
284
+ * `E`/`R` are the aggregate channels of this layout's `component` (with `Outlet`
285
+ * excluded) together with its whole subtree, so a sealed tree's channels are
286
+ * recoverable from the root.
287
+ */
288
+ interface LayoutNode<E = never, R = never> {
289
+ readonly _tag: "Layout";
290
+ readonly component: ComponentSlot;
291
+ readonly children: readonly TreeNode[];
292
+ /**
293
+ * Phantom marker for this layout subtree's aggregate error channel (see
294
+ * {@link TreeE}). Covariant (stores `E` directly) so a fully-discharged layout
295
+ * (`LayoutNode<never, never>` — its `Outlet` provided, no subtree errors) stays
296
+ * assignable to the `LayoutNode<any, any>` arm of {@link TreeNode}.
297
+ */
298
+ readonly _E?: E;
299
+ /** Phantom marker for this layout subtree's aggregate requirement channel (see {@link TreeR}). */
300
+ readonly _R?: R;
301
+ }
302
+ /** Any node in the route tree. */
303
+ type TreeNode = RouteNode<any, any, any, any> | LayoutNode<any, any>;
304
+ /** Extracts the error channel from a single {@link TreeNode}. */
305
+ type TreeE<T> = T extends RouteNode<any, any, infer E, any> ? E : T extends LayoutNode<infer E, any> ? E : never;
306
+ /** Extracts the requirement channel from a single {@link TreeNode}. */
307
+ type TreeR<T> = T extends RouteNode<any, any, any, infer R> ? R : T extends LayoutNode<any, infer R> ? R : never;
308
+ /** Aggregate error channel over a children tuple (distributes over `C[number]`). */
309
+ type SubtreeE<C extends readonly TreeNode[]> = TreeE<C[number]>;
310
+ /** Aggregate requirement channel over a children tuple (distributes over `C[number]`). */
311
+ type SubtreeR<C extends readonly TreeNode[]> = TreeR<C[number]>;
312
+ /**
313
+ * Declares a leaf page. The `component` *is* the route handler — a thunk the
314
+ * router invokes at render time; its error / requirement channels propagate up the
315
+ * tree. Two authoring forms are accepted:
316
+ *
317
+ * - **Handler-arg props** — the slot declares `(props: {@link RouteHandlerProps})`
318
+ * and the router passes the live match's decoded `{ path, query }` in directly
319
+ * (first overload; `path`/`query` are inferred from the route's `path`/`query`
320
+ * fields). A plain zero-arg thunk works too — it just ignores the props.
321
+ * - **Dependency injection** — a `Component.make` / `Component.gen` component that
322
+ * reads the live match via `Router.params` / `Router.query` (second overload).
323
+ *
324
+ * @example Handler-arg props (decoded `{ path, query }`)
325
+ * ```ts
326
+ * Router.route("users/:id", {
327
+ * path: { id: Schema.NumberFromString },
328
+ * query: { tab: Schema.optional(Schema.String) },
329
+ * component: ({ path, query }) => h.div({}, `User ${path.id} (${query.tab ?? "info"})`),
330
+ * });
331
+ * ```
332
+ *
333
+ * @example Dependency injection (`Router.params` / a `Component`)
334
+ * ```ts
335
+ * Router.route("about", { component: Component.make(() => h.h1({}, "About")) });
336
+ * Router.route("users/:id", {
337
+ * path: { id: Schema.NumberFromString },
338
+ * component: Component.gen(function* () {
339
+ * const { id } = yield* Router.params({ id: Schema.NumberFromString });
340
+ * return yield* h.div({}, `User ${id}`);
341
+ * }),
342
+ * });
343
+ * ```
344
+ */
345
+ declare function makeRoute<Path extends Fields = {}, Query extends Fields = {}, E = never, R = never>(segment: string, config: {
346
+ readonly path?: Path;
347
+ readonly query?: Query;
348
+ readonly component: (props: RouteHandlerProps<Path, Query>) => Node<E, R>;
349
+ }): RouteNode<Path, Query, E, R>;
350
+ declare function makeRoute<Path extends Fields = {}, Query extends Fields = {}, S extends ComponentSlot = ComponentSlot>(segment: string, config: {
351
+ readonly path?: Path;
352
+ readonly query?: Query;
353
+ readonly component: S;
354
+ }): RouteNode<Path, Query, Node.Error<SlotNode<S>>, Node.Context<SlotNode<S>>>;
355
+ /**
356
+ * Declares a layout. `component` is a {@link ComponentSlot} that splices the next
357
+ * level down via `yield* Router.Outlet` (place it in the returned tree). The
358
+ * router invokes it per render and provides that outlet, so `Router.Outlet` is
359
+ * **excluded** from the layout's aggregate requirement channel; the subtree's
360
+ * real channels are unioned in.
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * Router.layout(
365
+ * {
366
+ * component: Component.gen(function* () {
367
+ * const outlet = yield* Router.Outlet;
368
+ * return yield* h.div({ class: "shell" }, [Header(), outlet]);
369
+ * }),
370
+ * },
371
+ * [Router.route("", { component: Home })],
372
+ * );
373
+ * ```
374
+ */
375
+ declare function makeLayout<C extends readonly TreeNode[], S extends ComponentSlot = ComponentSlot>(config: {
376
+ readonly component: S;
377
+ }, children: C): LayoutNode<Node.Error<SlotNode<S>> | SubtreeE<C>, Exclude<Node.Context<SlotNode<S>>, Router.Outlet> | SubtreeR<C>>;
378
+ //#endregion
379
+ //#region src/compile.d.ts
380
+ /**
381
+ * A compiled layout level: its component slot plus the dedupe `patternPrefix` used
382
+ * by the client outlet to key the level. A layout owns no path of its own, so the
383
+ * prefix is derived as the **longest common path-segment prefix of every leaf in
384
+ * the layout's subtree** — it changes (and the level re-renders) exactly when a
385
+ * param shared by all those leaves changes, and persists otherwise.
386
+ */
387
+ interface CompiledLayout {
388
+ /** Longest common path prefix of the layout's subtree leaves, e.g. `/users/:id`. */
389
+ readonly patternPrefix: string;
390
+ /** Param names appearing in `patternPrefix`. */
391
+ readonly paramNames: readonly string[];
392
+ /** The layout's component slot; invoked per render with the outlet injected via `Router.Outlet`. */
393
+ readonly component: ComponentSlot;
394
+ }
395
+ /**
396
+ * A compiled leaf route: the flattened routing contract for one page. `pathSchema`
397
+ * merges every path field declared down the branch (leaf wins on collision) and
398
+ * covers every `:name` placeholder (defaulting to `Schema.String`).
399
+ */
400
+ interface CompiledLeaf {
401
+ /** Stable identifier derived from the full pattern; used as the HttpApi endpoint name. */
402
+ readonly id: string;
403
+ /** Full path pattern from the root, e.g. `/users/:id/settings` (root ⇒ `/`). */
404
+ readonly fullPathPattern: string;
405
+ /** Ordered param names in `fullPathPattern`. */
406
+ readonly paramNames: readonly string[];
407
+ /**
408
+ * Path-param schema. Its **encoded** side is typed string-encodeable
409
+ * (`Record<string, string | undefined>`) so it satisfies platform's
410
+ * `HttpApiEndpoint.setPath` constraint without an `as any` cast — param schemas
411
+ * round-trip strings, so the `Schema.Struct` value is asserted to this shape.
412
+ */
413
+ readonly pathSchema: Schema.Schema<Record<string, unknown>, Readonly<Record<string, string | undefined>>>;
414
+ /**
415
+ * Query schema. Its **encoded** side is typed string-encodeable
416
+ * (`Record<string, string | ReadonlyArray<string> | undefined>`) so it satisfies
417
+ * platform's `HttpApiEndpoint.setUrlParams` constraint without a cast.
418
+ */
419
+ readonly querySchema: Schema.Schema<Record<string, unknown>, Readonly<Record<string, string | ReadonlyArray<string> | undefined>>>;
420
+ /** The page's component slot; invoked per render, reads params via `Router.params` / `Router.query`. */
421
+ readonly component: ComponentSlot;
422
+ /** Ancestor layouts (root → parent) wrapping this leaf. */
423
+ readonly layoutChain: readonly CompiledLayout[];
424
+ }
425
+ /** The result of compiling a route tree: a flat leaf list plus the not-found page. */
426
+ interface Compiled {
427
+ readonly leaves: readonly CompiledLeaf[];
428
+ readonly notFound: () => Node<any, any>;
429
+ }
430
+ /**
431
+ * A sealed, compiled router definition. The unit passed to the client and server.
432
+ * `E`/`R` are phantom: they carry the aggregate error / requirement channels of
433
+ * the whole tree (plus the not-found page) so {@link RouterApp} / {@link outletNode}
434
+ * can surface a precise `Node` type instead of `Node<any, any>`.
435
+ */
436
+ interface RouterDef<E = any, R = any> {
437
+ readonly root: TreeNode;
438
+ readonly notFound: () => Node<any, any>;
439
+ readonly compiled: Compiled;
440
+ /**
441
+ * The authoritative `HttpApi` for this tree (one `"pages"` group, one GET
442
+ * endpoint per leaf). The single source of truth the server dispatch and the
443
+ * client matcher both derive from; {@link Compiled} carries only the
444
+ * nesting/render metadata platform's flat API can't represent. Built by
445
+ * {@link buildHttpApi} during {@link makeRouter}.
446
+ */
447
+ readonly httpApi: HttpApi.HttpApi.Any;
448
+ /**
449
+ * Phantom marker for the tree's aggregate error channel. Covariant (stores `E`
450
+ * directly) so a fully-static `RouterDef<never, never>` stays assignable to the
451
+ * `RouterDef<any, any>` arms used internally (mirrors {@link LayoutNode}).
452
+ */
453
+ readonly _E?: E;
454
+ /** Phantom marker for the tree's aggregate requirement channel. */
455
+ readonly _R?: R;
456
+ }
457
+ /** Options for {@link router}. */
458
+ interface RouterOptions<NF extends Node<any, any> = Node<any, any>> {
459
+ /** App-level not-found page, rendered when no route matches or a page raises `RouterNotFound`. */
460
+ readonly notFound: () => NF;
461
+ }
462
+ /**
463
+ * Maps each authored {@link RouteNode} to its {@link CompiledLeaf}. Populated by
464
+ * {@link compile} (via {@link router}) and read by `href` so a leaf reference can
465
+ * resolve its full pattern and schemas.
466
+ */
467
+ declare const leafRegistry: WeakMap<RouteNode<any, any, any, any>, CompiledLeaf>;
468
+ /**
469
+ * Compiles a route tree into a flat list of {@link CompiledLeaf}s (C1–C6).
470
+ *
471
+ * Pass 1 walks the tree: only **routes** contribute path parts (layouts own no
472
+ * path), so each leaf's `parts` come solely from the route segments on its branch,
473
+ * and its ancestor `LayoutNode`s are recorded in order. Pass 2 derives one shared
474
+ * {@link CompiledLayout} per distinct layout node — its `patternPrefix` is the
475
+ * longest common path prefix of that layout's subtree leaves — then assembles each
476
+ * leaf's `layoutChain` (root → parent) and merged path schema.
477
+ */
478
+ declare function compile(def: {
479
+ root: TreeNode;
480
+ notFound: () => Node<any, any>;
481
+ }): Compiled;
482
+ /**
483
+ * Builds the authoritative `HttpApi` for a compiled tree (S4): a single `"pages"`
484
+ * group whose endpoints are GET endpoints — one per leaf — at each leaf's full path
485
+ * pattern, carrying `setPath(pathSchema)`, `setUrlParams(querySchema)`, a
486
+ * `Schema.String` (text/HTML) success, and a `RouterNotFound → 404` error. The tree
487
+ * (not `HttpApi`) is the authoring surface; this is the single source of truth the
488
+ * server dispatch (`HttpApiBuilder`) and the client matcher / derived `HttpApiClient`
489
+ * read from, so both sides agree on paths and schemas.
490
+ *
491
+ * Each leaf's `pathSchema`/`querySchema` are typed string-encodeable (see
492
+ * {@link CompiledLeaf}), so `setPath`/`setUrlParams` need no `as any` casts.
493
+ *
494
+ * `Boundary.rpc` data no longer rides this spine: it resolves through the app's
495
+ * merged `RpcGroup` over the ambient `AppRpcClient` (`POST /_eui/rpc`), wired
496
+ * explicitly into `RouterServer`/`RouterLive`. The matcher reads only `"pages"`.
497
+ */
498
+ declare function buildHttpApi(leaves: readonly CompiledLeaf[]): HttpApi.HttpApi.Any;
499
+ /**
500
+ * Seals a route tree into a {@link RouterDef}, compiling it eagerly (so leaf
501
+ * references are stamped for `href`), building its authoritative {@link buildHttpApi}
502
+ * spine, and capturing the app-level not-found page. The tree's aggregate channels
503
+ * (plus the not-found page's) are carried on the returned `RouterDef`'s phantom
504
+ * `E`/`R` params.
505
+ */
506
+ declare function makeRouter<T extends TreeNode, NF extends Node<any, any> = Node>(root: T, options: RouterOptions<NF>): RouterDef<TreeE<T> | Node.Error<NF>, TreeR<T> | Node.Context<NF>>;
507
+ //#endregion
508
+ export { RouteMatch as C, RouterParamsError as D, RouterNotFound as E, isRouterNotFound as O, RouterHttpApiClient as S, match as T, TreeE as _, RouterOptions as a, NavigateOptions as b, leafRegistry as c, FieldsType as d, LayoutNode as f, SubtreeR as g, SubtreeE as h, RouterDef as i, notFound as k, ComponentSlot as l, RouteNode as m, CompiledLayout as n, buildHttpApi as o, RouteHandlerProps as p, CompiledLeaf as r, compile as s, Compiled as t, Fields as u, TreeNode as v, compileMatchers as w, Router as x, TreeR as y };
@@ -0,0 +1 @@
1
+ import{o as e}from"./outlet-C7p8KXmO.js";import{Either as t,Option as n,Schema as r}from"effect";function i(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function a(e){return e.split(`/`).filter(e=>e.startsWith(`:`)).map(e=>e.slice(1))}function o(e){return e.split(`/`).filter(e=>e.startsWith(`:`)).length}function s(e){let t=e.split(`/`).filter(e=>e.length>0).map(e=>e.startsWith(`:`)?`([^/]+)`:i(e)).join(`/`);return RegExp(t.length===0?`^/?$`:`^/${t}/?$`)}const c=r.Struct({}),l=new WeakMap;function u(e){let t=l.get(e);if(t!==void 0)return t;let r=new Map;for(let t of e.compiled.leaves)r.set(t.id,t);let i=e.httpApi.groups.pages?.endpoints??{},u=[];for(let e of Object.values(i)){let t=r.get(e.name);t!==void 0&&u.push({leaf:t,regex:s(e.path),paramNames:a(e.path),pathSchema:n.getOrElse(e.pathSchema,()=>c),querySchema:n.getOrElse(e.urlParamsSchema,()=>c)})}return u.sort((e,t)=>{let n=o(e.leaf.fullPathPattern)-o(t.leaf.fullPathPattern);return n===0?t.leaf.fullPathPattern.length-e.leaf.fullPathPattern.length:n}),l.set(e,u),u}function d(e){let t=e.indexOf(`#`),n=t===-1?e:e.slice(0,t),r=n.indexOf(`?`),i=r===-1?n:n.slice(0,r),a=r===-1?``:n.slice(r+1),o=i.length===0?`/`:i;return o.startsWith(`/`)||(o=`/${o}`),o.length>1&&o.endsWith(`/`)&&(o=o.slice(0,-1)),{path:o,search:a}}function f(e){let t={};if(e.length===0)return t;for(let[n,r]of new URLSearchParams(e))t[n]=r;return t}function p(e,n){let i=u(e),{path:a,search:o}=d(n),s=o.length===0?a:`${a}?${o}`;for(let e of i){let n=e.regex.exec(a);if(n===null)continue;let i={};e.paramNames.forEach((e,t)=>{let r=n[t+1];r!==void 0&&(i[e]=decodeURIComponent(r))});let c=r.decodeUnknownEither(e.pathSchema)(i);if(t.isLeft(c))continue;let l=r.decodeUnknownEither(e.querySchema)(f(o));if(!t.isLeft(l))return{_tag:`Matched`,leaf:e.leaf,path:c.right,query:l.right,url:s}}return{_tag:`NotFound`,url:s}}function m(t,...n){let i=e.get(t);if(i===void 0)throw Error(`href: route has not been compiled. Seal the tree with Router.router() before calling href().`);let{path:a={},query:o={}}=n[0]??{},s=r.encodeUnknownSync(i.pathSchema)(a),c=i.fullPathPattern.replace(/:([A-Za-z0-9_]+)/g,(e,t)=>encodeURIComponent(String(s[t]))),l=r.encodeUnknownSync(i.querySchema)(o),u=new URLSearchParams;for(let e of Object.keys(l).sort()){let t=l[e];t!=null&&u.append(e,String(t))}let d=u.toString();return d.length>0&&(c=`${c}?${d}`),c}export{u as n,p as r,m as t};
@@ -0,0 +1,3 @@
1
+ import { C as RouteMatch, D as RouterParamsError, E as RouterNotFound, O as isRouterNotFound, S as RouterHttpApiClient, T as match, _ as TreeE, a as RouterOptions, b as NavigateOptions, c as leafRegistry, d as FieldsType, f as LayoutNode, g as SubtreeR, h as SubtreeE, i as RouterDef, k as notFound, 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 compileMatchers, x as Router, y as TreeR } from "./compile-C0JShTTR.js";
2
+ import { i as href, n as outletNode, r as HrefArgs, t as RouterApp } from "./outlet-BBEjKu3z.js";
3
+ export { type Compiled, type CompiledLayout, type CompiledLeaf, type ComponentSlot, type Fields, type FieldsType, type HrefArgs, type LayoutNode, 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 ADDED
@@ -0,0 +1 @@
1
+ import{a as e,c as t,i as n,l as r,n as i,o as a,r as o,s,t as c,u as l}from"./outlet-C7p8KXmO.js";import{n as u,r as d,t as f}from"./href-Dl30XcF4.js";export{o as Router,c as RouterApp,s as RouterNotFound,t as RouterParamsError,n as buildHttpApi,e as compile,u as compileMatchers,f as href,r as isRouterNotFound,a as leafRegistry,d as match,l as notFound,i as outletNode};
@@ -0,0 +1,69 @@
1
+ import { E as RouterNotFound, d as FieldsType, i as RouterDef, m as RouteNode, u as Fields, x as Router } from "./compile-C0JShTTR.js";
2
+ import { Node } from "@weftui/core";
3
+
4
+ //#region src/href.d.ts
5
+ /**
6
+ * The `href` argument object for a leaf with path fields `Path` and query fields
7
+ * `Query`. `path`/`query` become optional when their decoded type has no required
8
+ * keys, and required otherwise (H4).
9
+ */
10
+ type HrefArgs<Path extends Fields, Query extends Fields> = ({} extends FieldsType<Path> ? {
11
+ readonly path?: FieldsType<Path>;
12
+ } : {
13
+ readonly path: FieldsType<Path>;
14
+ }) & ({} extends FieldsType<Query> ? {
15
+ readonly query?: FieldsType<Query>;
16
+ } : {
17
+ readonly query: FieldsType<Query>;
18
+ });
19
+ /**
20
+ * Builds a type-safe URL for a leaf route reference (the value returned by
21
+ * {@link route}). Path params are encoded into the pattern and query values are
22
+ * encoded into a key-sorted search string (H1–H4). Round-trips with `match`.
23
+ *
24
+ * The leaf must belong to a tree that has been sealed with `Router.router()`
25
+ * (which stamps the leaf registry); otherwise an error is thrown.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const userRoute = Router.route("users/:id", {
30
+ * path: { id: Schema.NumberFromString },
31
+ * component: …,
32
+ * });
33
+ * Router.router(Router.layout({ component: … }, [userRoute]), { notFound });
34
+ * href(userRoute, { path: { id: 42 } }); // "/users/42"
35
+ * ```
36
+ */
37
+ declare function href<Path extends Fields, Query extends Fields>(ref: RouteNode<Path, Query, any, any>, ...args: {} extends HrefArgs<Path, Query> ? [args?: HrefArgs<Path, Query>] : [args: HrefArgs<Path, Query>]): string;
38
+ //#endregion
39
+ //#region src/outlet.d.ts
40
+ /**
41
+ * The bare nested-outlet node for a router definition: a fragment whose single
42
+ * reactive child is the level-0 stream. Used directly by the server renderer so
43
+ * a `RouterNotFound` raised by a page escapes to the server's 404 handler.
44
+ */
45
+ declare function outletNode<E, R>(def: RouterDef<E, R>): Node<E | RouterNotFound, R | Router>;
46
+ /**
47
+ * The universal router root node. Wraps {@link outletNode} in the router's
48
+ * internal not-found boundary so a `RouterNotFound` raised by a page renders the
49
+ * configured `notFound` page in place (also covering client-side navigation). A
50
+ * user `Boundary.catchTag("RouterNotFound", …)` placed inside a page is nearer and
51
+ * therefore wins for that subtree.
52
+ *
53
+ * Server and client render the **same** `RouterApp` tree so hydration aligns. The
54
+ * server no longer needs a status side-channel: it dispatches through
55
+ * `HttpApiBuilder`, so a page-raised `RouterNotFound` (and a no-match) surface their
56
+ * 404 through the platform request pipeline rather than a render-time callback.
57
+ *
58
+ * `RouterLive` is a scoped layer (it owns the popstate listener + link click
59
+ * interceptor) and must outlive the mount, so provide it via a long-lived
60
+ * `ManagedRuntime` rather than `Effect.provide` at the node level:
61
+ *
62
+ * ```ts
63
+ * const runtime = ManagedRuntime.make(RouterLive(def));
64
+ * runtime.runPromise(hydrate(RouterApp(def), root));
65
+ * ```
66
+ */
67
+ declare function RouterApp<E, R>(def: RouterDef<E, R>): Node<Exclude<E, RouterNotFound>, R | Router>;
68
+ //#endregion
69
+ export { href as i, outletNode as n, HrefArgs as r, RouterApp as t };
Binary file
@@ -0,0 +1,66 @@
1
+ import { i as RouterDef, l as ComponentSlot } from "../compile-C0JShTTR.js";
2
+ import { Effect, Layer } from "effect";
3
+ import { RpcGroup } from "@effect/rpc";
4
+
5
+ //#region src/server/router-server.d.ts
6
+ /**
7
+ * Server-side rendering for a {@link RouterDef}. Dispatch runs through the
8
+ * authoritative `HttpApi` spine via `HttpApiBuilder`: platform owns request→leaf
9
+ * matching and path/query decode, then each leaf handler builds a fixed-match
10
+ * server `Router`, renders the universal outlet to hydratable HTML, and replies
11
+ * `text/html`. Status comes from the platform pipeline — a no-match (platform's
12
+ * `RouteNotFound`) and a page-raised `RouterNotFound` both render the configured
13
+ * `notFound` page at HTTP 404 — so there is no render-time status side-channel.
14
+ */
15
+ declare namespace RouterServer {
16
+ /**
17
+ * The app's `Boundary.rpc` data foundation: the merged `RpcGroup` contract plus
18
+ * its server-only handler `Layer`. Wired explicitly (no co-located `load`, no
19
+ * registry) — `toWebHandler` serves it at `POST /_eui/rpc`, and an in-process
20
+ * client over the same handlers resolves SSR boundaries in-process.
21
+ */
22
+ interface RpcOptions {
23
+ /** The app's merged `RpcGroup` (pure Schema contract; shared with the client). */
24
+ readonly group: RpcGroup.RpcGroup<any>;
25
+ /** The server-only handler Layer (`group.toLayer(...)` ⊕ its dependencies). */
26
+ readonly handlers: Layer.Layer<any, never, never>;
27
+ }
28
+ /**
29
+ * Shared server options. The document shell is a {@link ComponentSlot} that splices
30
+ * the app via `yield* Router.Outlet` (the router provides it per request) —
31
+ * typically `<html><head>…</head><body><div id="root">{app}</div><script …></body></html>`.
32
+ * The router provides both `Router.Outlet` (the app, per request) and `Router` (to read
33
+ * params), so the document may use either. Mirrors the route/layout `component` slot,
34
+ * so it accepts both a plain thunk and a `Component.make` / `Component.gen` component.
35
+ * `<!DOCTYPE html>` is prepended at serialize time.
36
+ */
37
+ interface Options {
38
+ /** The document shell slot; reads the app to splice via `yield* Router.Outlet`. */
39
+ readonly document: ComponentSlot;
40
+ /** The app's `Boundary.rpc` foundation (contract + server handlers). */
41
+ readonly rpc: RpcOptions;
42
+ }
43
+ /** The result of {@link render}. */
44
+ interface Rendered {
45
+ readonly html: string;
46
+ readonly status: number;
47
+ }
48
+ /**
49
+ * Renders the route matched by `options.url` to a hydratable HTML document
50
+ * (S1/S2) by driving the platform {@link webHandler}. Returns `{ html, status }`
51
+ * with `<!DOCTYPE html>` prepended and the status sourced from the platform
52
+ * pipeline (200, or 404 for a no-match / page-raised `RouterNotFound`).
53
+ */
54
+ function render(def: RouterDef, options: Options & {
55
+ readonly url: string;
56
+ }): Effect.Effect<Rendered, Error>;
57
+ /**
58
+ * The platform web `fetch`-style handler `(Request) => Promise<Response>` that
59
+ * dispatches through `HttpApiBuilder` and renders the matched route to
60
+ * `text/html`. Suitable for bridging into a dev server (e.g. Vite) or any
61
+ * Web-platform server.
62
+ */
63
+ function toWebHandler(def: RouterDef, options: Options): (request: Request) => Promise<Response>;
64
+ }
65
+ //#endregion
66
+ export { RouterServer };
@@ -0,0 +1 @@
1
+ import{l as e,n as t,r as n}from"../outlet-C7p8KXmO.js";import{HttpApiBuilder as r,HttpApiEndpoint as i,HttpApiGroup as a,HttpServer as o,HttpServerResponse as s}from"@effect/platform";import{Effect as c,Layer as l,Option as u,Schema as d,Stream as f,Subscribable as p}from"effect";import{AppRpcClientTag as m}from"@weftui/core";import{RpcSerialization as h,RpcServer as g,RpcTest as _}from"@effect/rpc";import{renderToStringHydratable as v}from"@weftui/dom/server";let y;(function(y){function b(e,t){return s.text(`<!DOCTYPE html>\n${e}`,{status:t,contentType:`text/html; charset=utf-8`})}function x(e){return n.of({currentMatch:p.make({get:c.succeed(e),changes:f.make(e)}),navigate:()=>c.void,httpApiClient:u.none()})}function S(e){return l.scoped(m,c.map(_.makeClient(e.group,{flatten:!0}),e=>m.of({call:(t,n)=>e(t,n)}))).pipe(l.provide(e.handlers))}function C(e,t,r){return v(c.provideService(e.document({}),n.Outlet,t)).pipe(c.provideService(n,r),c.provide(S(e.rpc)))}function w(e,t,n,r){let i=x({_tag:`NotFound`,url:n});return C(t,e.compiled.notFound(),i).pipe(c.map(e=>b(e,r)))}function T(e,n,r){let i=x({_tag:`NotFound`,url:r});return C(n,t(e),i).pipe(c.map(e=>b(e,404)))}function E(n,r,i){let a=x(i);return C(r,t(n),a).pipe(c.map(e=>b(e,200)),c.catchIf(e,()=>w(n,r,i.url,404)))}let D=new WeakMap;function O(e,t){let n=D.get(e)??new WeakMap;D.set(e,n);let s=n.get(t.document);if(s!==void 0)return s;let c=e.compiled.leaves,u=r,f=a.make(`fallback`).add(i.get(`catchAll`,`*`).addSuccess(d.String)),p=e.httpApi.add(f),m=u.group(p,`pages`,n=>c.reduce((n,r)=>n.handle(r.id,n=>E(e,t,{_tag:`Matched`,leaf:r,path:n.path,query:n.urlParams,url:n.request.url})),n)),_=u.group(p,`fallback`,n=>n.handle(`catchAll`,n=>T(e,t,n.request.url))),v=u.api(p).pipe(l.provide(l.mergeAll(m,_))),{handler:y}=r.toWebHandler(l.mergeAll(v,o.layerContext)),{handler:b}=g.toWebHandler(t.rpc.group,{layer:l.mergeAll(t.rpc.handlers,h.layerJson)}),x=e=>new URL(e.url).pathname===`/_eui/rpc`?b(e):y(e);return n.set(t.document,x),x}function k(e){return e.startsWith(`http://`)||e.startsWith(`https://`)?e:`http://localhost${e.startsWith(`/`)?e:`/${e}`}`}function A(e,t){return c.tryPromise({try:async()=>{let n=await O(e,t)(new Request(k(t.url)));return{html:await n.text(),status:n.status}},catch:e=>e instanceof Error?e:Error(String(e))})}y.render=A;function j(e,t){return O(e,t)}y.toWebHandler=j})(y||={});export{y as RouterServer};
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@weftui/router",
3
+ "version": "0.0.0",
4
+ "description": "Universal nested router for Weft",
5
+ "license": "MIT",
6
+ "author": "Stef van Wijchen",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "type": "module",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ },
16
+ "./client": {
17
+ "types": "./dist/client/index.d.ts",
18
+ "import": "./dist/client/index.js"
19
+ },
20
+ "./server": {
21
+ "types": "./dist/server/index.d.ts",
22
+ "import": "./dist/server/index.js"
23
+ }
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "dependencies": {
29
+ "@effect/platform": "^0.96.1",
30
+ "@effect/rpc": "^0.75.1",
31
+ "@weftui/core": "0.0.0",
32
+ "@weftui/dom": "0.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/jsdom": "^28.0.3",
36
+ "@types/node": "^25.9.1",
37
+ "effect": "^3.21.2",
38
+ "jsdom": "^29.1.1",
39
+ "tsx": "^4.22.3",
40
+ "typescript": "^6.0.3",
41
+ "vite-plus": "latest"
42
+ },
43
+ "peerDependencies": {
44
+ "effect": "^3.21"
45
+ }
46
+ }