@weftui/router 0.20.9 → 0.22.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.
@@ -1,10 +1,32 @@
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";
1
+ import { S as Router, b as NavState, i as RouterDef, m as RouteNode, u as Fields, x as NavigateOptions } from "../compile-HOyeyWRy.js";
2
+ import { n as outletNode, r as HrefArgs, t as RouterApp } from "../outlet-DbqTgkXa.js";
3
3
  import { Effect, Layer, Scope } from "effect";
4
4
  import { AppRpcClientTag } from "@weftui/core";
5
5
  import { RpcGroup } from "@effect/rpc";
6
6
 
7
7
  //#region src/client/router-live.d.ts
8
+ /**
9
+ * The residual app services a caller must still provide through the {@link RouterLiveOptions.context}
10
+ * seam — a def's aggregate `R` minus the services `RouterLive` already threads
11
+ * (`Router`, `Router.Outlet`, `AppRpcClientTag`). The client mirror of
12
+ * `RouterServer.AppServices`; resolves to `never` for an app with no app-wide service.
13
+ */
14
+ type AppServices<R> = Exclude<R, Router | Router.Outlet | AppRpcClientTag>;
15
+ /** True only for the exact `any` type — a loosely-typed `RouterDef<any, any>`. */
16
+ type IsAny<T> = 0 extends 1 & T ? true : false;
17
+ /**
18
+ * Conditionally shapes the `context` field: **required** when the def has statically
19
+ * known residual {@link AppServices}, **absent** when it has none, and **optional**
20
+ * for a loosely-typed `RouterDef<any, any>`. Client parity with the server seam (AC4)
21
+ * is thus a compile-time guarantee, and no-service / loosely-typed apps stay unchanged (AC3).
22
+ */
23
+ type ContextOption<R> = [AppServices<R>] extends [never] ? {
24
+ readonly context?: undefined;
25
+ } : IsAny<AppServices<R>> extends true ? {
26
+ readonly context?: Layer.Layer<any, never, never>;
27
+ } : {
28
+ readonly context: Layer.Layer<AppServices<R>, never, never>;
29
+ };
8
30
  /** Options for {@link RouterLive}. */
9
31
  interface RouterLiveOptions {
10
32
  /**
@@ -17,9 +39,11 @@ interface RouterLiveOptions {
17
39
  * The app's `Boundary.rpc` foundation: the merged `RpcGroup` contract (shared
18
40
  * with the server handler Layer). Backs the {@link AppRpcClientTag} seam so a
19
41
  * hydrated boundary refetch — and a client-first SPA mount — resolve over the
20
- * network rpc client.
42
+ * network rpc client. Optional: omit when the app has no `Boundary.rpc` — then
43
+ * no network rpc client is built and a stray `Boundary.rpc` fails with a
44
+ * descriptive error.
21
45
  */
22
- readonly rpc: {
46
+ readonly rpc?: {
23
47
  /** The app's merged `RpcGroup` (pure Schema contract). */readonly group: RpcGroup.RpcGroup<any>;
24
48
  };
25
49
  }
@@ -41,7 +65,7 @@ interface RouterLiveOptions {
41
65
  * refetch and client-first mount) without depending on this package or
42
66
  * `@effect/rpc`.
43
67
  */
44
- declare function RouterLive(def: RouterDef, options: RouterLiveOptions): Layer.Layer<Router | AppRpcClientTag>;
68
+ declare function RouterLive<R>(def: RouterDef<any, R>, options?: RouterLiveOptions & ContextOption<R>): Layer.Layer<Router | AppRpcClientTag | AppServices<R>>;
45
69
  //#endregion
46
70
  //#region src/client/link.d.ts
47
71
  /**
@@ -106,4 +130,4 @@ declare const setQuery: (query: Record<string, unknown>, options?: NavigateOptio
106
130
  */
107
131
  declare const patchQuery: (partial: Record<string, unknown>, options?: NavigateOptions) => Effect.Effect<void, never, Router>;
108
132
  //#endregion
109
- export { Router, RouterApp, RouterLive, outletNode as RouterOutlet, outletNode, back, forward, installLinkInterceptor, navigate, patchQuery, push, replace, setQuery };
133
+ export { type NavState, Router, RouterApp, RouterLive, outletNode as RouterOutlet, outletNode, back, forward, installLinkInterceptor, navigate, patchQuery, push, replace, setQuery };
@@ -1 +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};
1
+ import{i as e,n as t,r as n,t as r}from"../outlet-BS31W71_.js";import{r as i,t as a}from"../href-BgbDo3hx.js";import{FetchHttpClient as o,HttpApiClient as s}from"@effect/platform";import{Context as c,Effect as l,Layer as u,Option as d,Runtime as f,Schema as p,Stream as m,Subscribable as h,SubscriptionRef as g}from"effect";import{AppRpcClientTag as _}from"@weftui/core";function v(e,t){return l.gen(function*(){let n=yield*l.runtime(),r=r=>{if(r.defaultPrevented||r.button!==0||r.metaKey||r.ctrlKey||r.shiftKey||r.altKey)return;let a=r.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}`&&i(e,u)._tag===`Matched`&&(r.preventDefault(),f.runFork(n)(t(u)))};yield*l.acquireRelease(l.sync(()=>document.addEventListener(`click`,r)),()=>l.sync(()=>document.removeEventListener(`click`,r)))})}function y(){return`${window.location.pathname}${window.location.search}`}function b(e){let t=new URL(e,window.location.href);return`${t.pathname}${t.search}`}function x(t,r={}){let a=u.scopedContext(l.gen(function*(){let a=yield*g.make(y()),p=yield*g.make({_tag:`Idle`}),x=yield*l.runtime(),S=yield*s.make(t.httpApi,{baseUrl:r.baseUrl??window.location.origin}).pipe(l.provide(o.layer)),C=0,w=t=>t._tag===`Matched`?[t.leaf.component,...t.leaf.layoutChain.map(e=>e.component)].map(e).filter(e=>e!==void 0):[],T=(e,t)=>l.sync(()=>{t?window.history.replaceState(null,``,e):window.history.pushState(null,``,e)}),E=(e,n,r)=>l.gen(function*(){let o=w(i(t,e));if(o.length===0){n&&(yield*T(e,r)),yield*g.set(a,e);return}let s=++C;yield*g.set(p,{_tag:`Navigating`,to:e}),yield*l.promise(()=>Promise.all(o.map(e=>e()))).pipe(l.onError(()=>s===C?g.set(p,{_tag:`Idle`}):l.void)),s===C&&(n&&(yield*T(e,r)),yield*g.set(a,e),yield*g.set(p,{_tag:`Idle`}))}),D=(e,t)=>E(b(e),!0,t?.replace===!0),O=()=>{f.runFork(x)(E(y(),!1,!1))};yield*l.acquireRelease(l.sync(()=>window.addEventListener(`popstate`,O)),()=>l.sync(()=>window.removeEventListener(`popstate`,O))),yield*v(t,D);let k=h.make({get:l.map(g.get(a),e=>i(t,e)),changes:m.map(a.changes,e=>i(t,e))}),A=r.rpc,j;if(A===void 0)j=_.of({call:e=>l.fail(Error(`Boundary.rpc "${e}" cannot resolve: no \`rpc\` option was passed to RouterLive`))});else{let{RpcClient:e,RpcSerialization:t}=yield*l.promise(()=>import(`@effect/rpc`)),n=String(r.baseUrl??window.location.origin).replace(/\/$/,``),i=yield*e.make(A.group,{flatten:!0}).pipe(l.provide(e.layerProtocolHttp({url:`${n}/_eui/rpc`}).pipe(u.provide(u.mergeAll(o.layer,t.layerJson)))));j=_.of({call:(e,t)=>i(e,t)})}let M=n.of({currentMatch:k,navigate:D,httpApiClient:d.some(S),navigating:p});return c.make(n,M).pipe(c.add(_,j))})),p=r.context;return p===void 0?a:u.merge(a,p)}function S(e,...t){let[r,i]=t,o=a(e,r);return l.flatMap(n,e=>e.navigate(o,i))}const C=e=>l.flatMap(n,t=>t.navigate(e)),w=e=>l.flatMap(n,t=>t.navigate(e,{replace:!0})),T=()=>l.sync(()=>window.history.go(-1)),E=()=>l.sync(()=>window.history.go(1));function D(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 O(e){let t=e.indexOf(`?`);return t===-1?e:e.slice(0,t)}function k(e,t){return l.gen(function*(){let r=yield*n,i=yield*r.currentMatch.get;if(i._tag!==`Matched`)return;let a=e(i.query),o=D(p.encodeUnknownSync(i.leaf.querySchema)(a)),s=O(i.url);yield*r.navigate(o.length>0?`${s}?${o}`:s,t)})}const A=(e,t)=>k(()=>e,t),j=(e,t)=>k(t=>({...t,...e}),t);export{n as Router,r as RouterApp,x as RouterLive,t as RouterOutlet,t as outletNode,T as back,E as forward,v as installLinkInterceptor,S as navigate,j as patchQuery,C as push,w as replace,A as setQuery};
@@ -121,6 +121,19 @@ declare function match(def: RouterDef, url: string): RouteMatch;
121
121
  * (`RouterLive`), absent (`Option.none`) on the server, which is itself the origin.
122
122
  */
123
123
  type RouterHttpApiClient = HttpApiClient.Client<any, any, never>;
124
+ /**
125
+ * The client navigation state exposed reactively as {@link Router.navigating}. A
126
+ * navigation to a `Router.lazy` route is **deferred-commit** (see
127
+ * `pending-navigation.specs.md`): while its chunk resolves the state is
128
+ * `Navigating` (carrying the target `to`), returning to `Idle` on commit. An
129
+ * eager-route navigation stays `Idle`. Always `Idle` on the server (buffered render).
130
+ */
131
+ type NavState = {
132
+ readonly _tag: "Idle";
133
+ } | {
134
+ readonly _tag: "Navigating";
135
+ readonly to: string;
136
+ };
124
137
  /** Options for a router {@link Router} `navigate` call. */
125
138
  interface NavigateOptions {
126
139
  /**
@@ -145,6 +158,13 @@ declare const Router_base: Context.TagClass<Router, "@weftui/router/Router", {
145
158
  * it stays local via the shared matcher (see the refactor _Feasibility constraint_).
146
159
  */
147
160
  readonly httpApiClient: Option.Option<RouterHttpApiClient>;
161
+ /**
162
+ * Reactive client navigation state (see {@link NavState}). `Navigating{to}` while
163
+ * a deferred-commit navigation resolves its lazy chunk(s), `Idle` otherwise. An
164
+ * app reads it via {@link Router.navigatingStream} to render pending UI (e.g. a
165
+ * top progress bar). Constant `Idle` on the server.
166
+ */
167
+ readonly navigating: Subscribable.Subscribable<NavState>;
148
168
  }>;
149
169
  declare class Router extends Router_base {}
150
170
  declare const OutletTag_base: Context.TagClass<OutletTag, "@weftui/router/Outlet", Node<never, never>>;
@@ -198,6 +218,8 @@ declare namespace Router {
198
218
  const route: typeof makeRoute;
199
219
  /** Declares a layout wrapping an injected outlet. See {@link makeLayout}. */
200
220
  const layout: typeof makeLayout;
221
+ /** Wraps a dynamic-import loader as a lazy component slot. See {@link lazyComponent}. */
222
+ const lazy: typeof lazyComponent;
201
223
  /** Seals a route tree into a `RouterDef`. See {@link makeRouter}. */
202
224
  const router: typeof makeRouter;
203
225
  /** The injected outlet service value (yieldable Tag). See {@link OutletTag}. */
@@ -212,6 +234,8 @@ declare namespace Router {
212
234
  const paramsStream: typeof subscribeParams;
213
235
  /** Reactive {@link Subscribable} of the live match's query. See {@link subscribeQuery}. */
214
236
  const queryStream: typeof subscribeQuery;
237
+ /** Reactive {@link Subscribable} of the client navigation state. See {@link subscribeNavigating}. */
238
+ const navigatingStream: Effect.Effect<Subscribable.Subscribable<NavState, never, never>, never, Router>;
215
239
  }
216
240
  //#endregion
217
241
  //#region src/route-tree.d.ts
@@ -375,6 +399,28 @@ declare function makeRoute<Path extends Fields = {}, Query extends Fields = {},
375
399
  declare function makeLayout<C extends readonly TreeNode[], S extends ComponentSlot = ComponentSlot>(config: {
376
400
  readonly component: S;
377
401
  }, children: C): LayoutNode<Node.Error<SlotNode<S>> | SubtreeE<C>, Exclude<Node.Context<SlotNode<S>>, Router.Outlet> | SubtreeR<C>>;
402
+ /**
403
+ * Wraps a dynamic-import `load` as a lazy {@link ComponentSlot}: the route's descriptor
404
+ * (`segment`, `path`/`query`) stays eager and matchable, while the component — the render
405
+ * body and its module's deps — is split into the chunk `load` resolves. The router invokes
406
+ * the returned slot at render time; it awaits `load` then renders the resolved component,
407
+ * adopting the server DOM in place on hydration (flash-free) and fetching the chunk on
408
+ * client navigation. Exposed as {@link Router.lazy}. See `lazy-component.specs.md`.
409
+ *
410
+ * The resolved value is a component slot (`Component.gen` / `Component.make`, or a
411
+ * `() => Node` thunk) — the shape `component:` already accepts — so its `E`/`R` channels
412
+ * are recovered via {@link SlotNode} and propagate up the tree exactly as an eager
413
+ * component's do.
414
+ *
415
+ * @example
416
+ * ```ts
417
+ * Router.route("docs/:category/:slug", {
418
+ * path: { category: Schema.String, slug: Schema.String },
419
+ * component: Router.lazy(() => import("./doc-page").then((m) => m.DocPage)),
420
+ * });
421
+ * ```
422
+ */
423
+ declare function lazyComponent<S extends ComponentSlot>(load: () => Promise<S>): () => Node<Node.Error<SlotNode<S>>, Node.Context<SlotNode<S>>>;
378
424
  //#endregion
379
425
  //#region src/compile.d.ts
380
426
  /**
@@ -505,4 +551,4 @@ declare function buildHttpApi(leaves: readonly CompiledLeaf[]): HttpApi.HttpApi.
505
551
  */
506
552
  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
553
  //#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 };
554
+ export { notFound as A, RouterHttpApiClient as C, RouterNotFound as D, match as E, RouterParamsError as O, Router as S, compileMatchers as T, TreeE as _, RouterOptions as a, NavState as b, leafRegistry as c, FieldsType as d, LayoutNode as f, SubtreeR as g, SubtreeE as h, RouterDef as i, isRouterNotFound 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, RouteMatch as w, NavigateOptions as x, TreeR as y };
@@ -1 +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};
1
+ import{s as e}from"./outlet-BS31W71_.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};
package/dist/index.d.ts CHANGED
@@ -1,3 +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 };
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-HOyeyWRy.js";
2
+ import { i as href, n as outletNode, r as HrefArgs, t as RouterApp } from "./outlet-DbqTgkXa.js";
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 +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};
1
+ import{a as e,c as t,d as n,l as r,n as i,o as a,r as o,s,t as c,u as l}from"./outlet-BS31W71_.js";import{n as u,r as d,t as f}from"./href-BgbDo3hx.js";export{o as Router,c as RouterApp,t as RouterNotFound,r as RouterParamsError,e as buildHttpApi,a as compile,u as compileMatchers,f as href,l as isRouterNotFound,s as leafRegistry,d as match,n as notFound,i as outletNode};
@@ -1,4 +1,4 @@
1
- import { E as RouterNotFound, d as FieldsType, i as RouterDef, m as RouteNode, u as Fields, x as Router } from "./compile-C0JShTTR.js";
1
+ import { D as RouterNotFound, S as Router, d as FieldsType, i as RouterDef, m as RouteNode, u as Fields } from "./compile-HOyeyWRy.js";
2
2
  import { Node } from "@weftui/core";
3
3
 
4
4
  //#region src/href.d.ts
@@ -1,5 +1,6 @@
1
- import { i as RouterDef, l as ComponentSlot } from "../compile-C0JShTTR.js";
1
+ import { S as Router, i as RouterDef, l as ComponentSlot } from "../compile-HOyeyWRy.js";
2
2
  import { Effect, Layer } from "effect";
3
+ import { AppRpcClientTag } from "@weftui/core";
3
4
  import { RpcGroup } from "@effect/rpc";
4
5
 
5
6
  //#region src/server/router-server.d.ts
@@ -19,7 +20,7 @@ declare namespace RouterServer {
19
20
  * registry) — `toWebHandler` serves it at `POST /_eui/rpc`, and an in-process
20
21
  * client over the same handlers resolves SSR boundaries in-process.
21
22
  */
22
- interface RpcOptions {
23
+ export interface RpcOptions {
23
24
  /** The app's merged `RpcGroup` (pure Schema contract; shared with the client). */
24
25
  readonly group: RpcGroup.RpcGroup<any>;
25
26
  /** The server-only handler Layer (`group.toLayer(...)` ⊕ its dependencies). */
@@ -33,15 +34,53 @@ declare namespace RouterServer {
33
34
  * params), so the document may use either. Mirrors the route/layout `component` slot,
34
35
  * so it accepts both a plain thunk and a `Component.make` / `Component.gen` component.
35
36
  * `<!DOCTYPE html>` is prepended at serialize time.
37
+ *
38
+ * `context` is the render-time provide seam (spec:
39
+ * `ambient-context-propagation.specs.md`): an app-wide `Layer` provided to the
40
+ * document shell **and** every route/layout leaf. It is required exactly when the
41
+ * def carries residual app services ({@link AppServices} — its aggregate `R` minus
42
+ * the `Router` / `Router.Outlet` / `AppRpcClientTag` the router already threads) and
43
+ * disallowed otherwise, so a missing provide is a compile error and `rpc`-only /
44
+ * no-service apps stay unchanged (surfaced via {@link ContextOption} at each entry).
36
45
  */
37
- interface Options {
46
+ export interface Options {
38
47
  /** The document shell slot; reads the app to splice via `yield* Router.Outlet`. */
39
48
  readonly document: ComponentSlot;
40
- /** The app's `Boundary.rpc` foundation (contract + server handlers). */
41
- readonly rpc: RpcOptions;
49
+ /**
50
+ * The app's `Boundary.rpc` foundation (contract + server handlers). Optional:
51
+ * omit when the app has no `Boundary.rpc` — then `POST /_eui/rpc` is not
52
+ * served (it falls through to page dispatch) and a stray `Boundary.rpc`
53
+ * fails with a descriptive error instead of rendering.
54
+ */
55
+ readonly rpc?: RpcOptions;
42
56
  }
57
+ /**
58
+ * The residual app services a caller must still provide through the {@link Options.context}
59
+ * seam: a def's aggregate requirement `R` **minus** the services the router already
60
+ * threads in per render — `Router` and `Router.Outlet` (provided by the outlet /
61
+ * document plumbing) and `AppRpcClientTag` (provided from the `rpc` option). When this
62
+ * resolves to `never` the app has no app-wide service to inject and `context` is disallowed.
63
+ */
64
+ export type AppServices<R> = Exclude<R, Router | Router.Outlet | AppRpcClientTag>;
65
+ /** True only for the exact `any` type — a loosely-typed `RouterDef<any, any>`. */
66
+ type IsAny<T> = 0 extends 1 & T ? true : false;
67
+ /**
68
+ * Conditionally shapes the `context` field at each entry point: **required** (a
69
+ * `Layer` supplying every residual {@link AppServices}) when the def has statically
70
+ * known app-wide services, **absent** when it has none, and **optional** for a
71
+ * loosely-typed `RouterDef<any, any>` (residual services can't be tracked, so the
72
+ * seam is not forced). This makes a missing provide a compile error for a precisely
73
+ * typed def (AC2) while keeping no-service / loosely-typed apps unchanged (AC3).
74
+ */
75
+ export type ContextOption<R> = [AppServices<R>] extends [never] ? {
76
+ readonly context?: undefined;
77
+ } : IsAny<AppServices<R>> extends true ? {
78
+ readonly context?: Layer.Layer<any, never, never>;
79
+ } : {
80
+ readonly context: Layer.Layer<AppServices<R>, never, never>;
81
+ };
43
82
  /** The result of {@link render}. */
44
- interface Rendered {
83
+ export interface Rendered {
45
84
  readonly html: string;
46
85
  readonly status: number;
47
86
  }
@@ -51,16 +90,30 @@ declare namespace RouterServer {
51
90
  * with `<!DOCTYPE html>` prepended and the status sourced from the platform
52
91
  * pipeline (200, or 404 for a no-match / page-raised `RouterNotFound`).
53
92
  */
54
- function render(def: RouterDef, options: Options & {
93
+ export function render<R>(def: RouterDef<any, R>, options: Options & {
55
94
  readonly url: string;
56
- }): Effect.Effect<Rendered, Error>;
95
+ } & ContextOption<R>): Effect.Effect<Rendered, Error>;
57
96
  /**
58
97
  * The platform web `fetch`-style handler `(Request) => Promise<Response>` that
59
98
  * dispatches through `HttpApiBuilder` and renders the matched route to
60
99
  * `text/html`. Suitable for bridging into a dev server (e.g. Vite) or any
61
100
  * Web-platform server.
62
101
  */
63
- function toWebHandler(def: RouterDef, options: Options): (request: Request) => Promise<Response>;
102
+ export function toWebHandler<R>(def: RouterDef<any, R>, options: Options & ContextOption<R>): (request: Request) => Promise<Response>;
103
+ /**
104
+ * Streaming variant of {@link toWebHandler} (spec: "Streaming SSR" in
105
+ * `router.specs.md`, SW1 … SW7). Same `Options`, memoized separately. Per
106
+ * matched leaf the document renders via the dom shell-split API
107
+ * (`renderToHydratableShell`): the buffered shell decides the HTTP status
108
+ * before any bytes flush, then streams as the first chunk with the Suspense
109
+ * patch chunks after it. A `RouterNotFound` escaping `Boundary.suspend`
110
+ * children after the flush keeps 200 and patches in the `notFound` page plus
111
+ * a noindex robots meta (soft-404). No-match and shell-raised
112
+ * `RouterNotFound` stay real 404s; `POST /_eui/rpc` delegation is unchanged.
113
+ * `render` and `toWebHandler` remain fully buffered.
114
+ */
115
+ export function toStreamingWebHandler<R>(def: RouterDef<any, R>, options: Options & ContextOption<R>): (request: Request) => Promise<Response>;
116
+ export {};
64
117
  }
65
118
  //#endregion
66
119
  export { RouterServer };
@@ -1 +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};
1
+ import{c as e,n as t,r as n,u as r}from"../outlet-BS31W71_.js";import{HttpApiBuilder as i,HttpApiEndpoint as a,HttpApiGroup as o,HttpServer as s,HttpServerResponse as c}from"@effect/platform";import{Cause as l,Effect as u,Exit as d,Layer as f,Option as p,Schema as m,Scope as h,Stream as g,Subscribable as _}from"effect";import{AppRpcClientTag as v}from"@weftui/core";import{SuspenseFailureHandlerTag as y,renderToHydratableShell as b,renderToStringHydratable as x}from"@weftui/dom/server";import{RpcSerialization as S,RpcServer as C,RpcTest as w}from"@effect/rpc";let T;(function(T){function E(e,t){return c.text(`<!DOCTYPE html>\n${e}`,{status:t,contentType:`text/html; charset=utf-8`})}function D(e){let t={_tag:`Idle`};return n.of({currentMatch:_.make({get:u.succeed(e),changes:g.make(e)}),navigate:()=>u.void,httpApiClient:p.none(),navigating:_.make({get:u.succeed(t),changes:g.make(t)})})}function O(e){return e===void 0?f.succeed(v,v.of({call:e=>u.fail(Error(`Boundary.rpc "${e}" cannot resolve: no \`rpc\` option was passed to RouterServer`))})):f.scoped(v,u.map(w.makeClient(e.group,{flatten:!0}),e=>v.of({call:(t,n)=>e(t,n)}))).pipe(f.provide(e.handlers))}function k(e,t,r){return x(u.provideService(e.document({}),n.Outlet,t)).pipe(u.provideService(n,r),u.provide(O(e.rpc)),u.provide(e.context??f.empty))}function A(e,t,n,r){let i=D({_tag:`NotFound`,url:n});return k(t,e.compiled.notFound(),i).pipe(u.map(e=>E(e,r)))}function j(e,n,r){let i=D({_tag:`NotFound`,url:r});return k(n,t(e),i).pipe(u.map(e=>E(e,404)))}function M(e,n,i){let a=D(i);return k(n,t(e),a).pipe(u.map(e=>E(e,200)),u.catchIf(r,()=>A(e,n,i.url,404)))}function N(t){return{handle:n=>{let i=l.failureOption(n);return p.isSome(i)&&r(i.value)?p.some({content:t.compiled.notFound(),markNoindex:!0,failureReplay:m.encodeSync(e)(i.value)}):p.none()}}}function P(e,i,a){let o=D(a),s=t(e);return u.gen(function*(){let t=yield*h.make(),{shell:r,patches:a}=yield*b(u.provideService(i.document({}),n.Outlet,s)).pipe(u.provideService(n,o),u.provideService(y,N(e)),u.provide(O(i.rpc)),u.provide(i.context??f.empty),h.extend(t),u.onError(e=>h.close(t,d.failCause(e)))),l=g.make(`<!DOCTYPE html>\n${r}`).pipe(g.concat(a),g.ensuring(h.close(t,d.void)),g.encodeText);return c.stream(l,{status:200,contentType:`text/html; charset=utf-8`})}).pipe(u.catchIf(r,()=>A(e,i,a.url,404)))}let F=new WeakMap,I=new WeakMap;function L(e,t,n,r){let c=n.get(e)??new WeakMap;n.set(e,c);let l=c.get(t.document);if(l!==void 0)return l;let u=e.compiled.leaves,d=i,p=o.make(`fallback`).add(a.get(`catchAll`,`*`).addSuccess(m.String)),h=e.httpApi.add(p),g=d.group(h,`pages`,n=>u.reduce((n,i)=>n.handle(i.id,n=>r(e,t,{_tag:`Matched`,leaf:i,path:n.path,query:n.urlParams,url:n.request.url})),n)),_=d.group(h,`fallback`,n=>n.handle(`catchAll`,n=>j(e,t,n.request.url))),v=d.api(h).pipe(f.provide(f.mergeAll(g,_))),{handler:y}=i.toWebHandler(f.mergeAll(v,s.layerContext)),b=t.rpc,x=y;if(b!==void 0){let{handler:e}=C.toWebHandler(b.group,{layer:f.mergeAll(b.handlers,S.layerJson)});x=t=>new URL(t.url).pathname===`/_eui/rpc`?e(t):y(t)}return c.set(t.document,x),x}function R(e,t){return L(e,t,F,M)}function z(e){return e.startsWith(`http://`)||e.startsWith(`https://`)?e:`http://localhost${e.startsWith(`/`)?e:`/${e}`}`}function B(e,t){let n=t;return u.tryPromise({try:async()=>{let t=await R(e,n)(new Request(z(n.url)));return{html:await t.text(),status:t.status}},catch:e=>e instanceof Error?e:Error(String(e))})}T.render=B;function V(e,t){return R(e,t)}T.toWebHandler=V;function H(e,t){return L(e,t,I,P)}T.toStreamingWebHandler=H})(T||={});export{T as RouterServer};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weftui/router",
3
- "version": "0.20.9",
3
+ "version": "0.22.0",
4
4
  "description": "Universal nested router for Weft",
5
5
  "license": "MIT",
6
6
  "author": "Stef van Wijchen",
@@ -32,17 +32,17 @@
32
32
  "dependencies": {
33
33
  "@effect/platform": "^0.96.1",
34
34
  "@effect/rpc": "^0.75.1",
35
- "@weftui/core": "0.20.9",
36
- "@weftui/dom": "0.20.9"
35
+ "@weftui/core": "0.22.0",
36
+ "@weftui/dom": "0.22.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/jsdom": "^28.0.3",
40
- "@types/node": "^25.9.1",
41
- "effect": "^3.21.2",
40
+ "@types/node": "^25.9.2",
41
+ "effect": "^3.21.3",
42
42
  "jsdom": "^29.1.1",
43
- "tsx": "^4.22.3",
43
+ "tsx": "^4.22.4",
44
44
  "typescript": "^6.0.3",
45
- "vite-plus": "latest"
45
+ "vite-plus": "0.2.1"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "effect": "^3.21"