@weftui/router 0.21.0 → 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
  /**
@@ -43,7 +65,7 @@ interface RouterLiveOptions {
43
65
  * refetch and client-first mount) without depending on this package or
44
66
  * `@effect/rpc`.
45
67
  */
46
- 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>>;
47
69
  //#endregion
48
70
  //#region src/client/link.d.ts
49
71
  /**
@@ -108,4 +130,4 @@ declare const setQuery: (query: Record<string, unknown>, options?: NavigateOptio
108
130
  */
109
131
  declare const patchQuery: (partial: Record<string, unknown>, options?: NavigateOptions) => Effect.Effect<void, never, Router>;
110
132
  //#endregion
111
- 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=n.rpc,D;if(E===void 0)D=g.of({call:e=>c.fail(Error(`Boundary.rpc "${e}" cannot resolve: no \`rpc\` option was passed to RouterLive`))});else{let e=String(n.baseUrl??window.location.origin).replace(/\/$/,``),t=yield*_.make(E.group,{flatten:!0}).pipe(c.provide(_.layerProtocolHttp({url:`${e}/_eui/rpc`}).pipe(l.provide(l.mergeAll(a.layer,v.layerJson)))));D=g.of({call:(e,n)=>t(e,n)})}let O=t.of({currentMatch:T,navigate:w,httpApiClient:u.some(S)});return s.make(t,O).pipe(s.add(g,D))}))}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,8 +34,16 @@ 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
49
  /**
@@ -45,8 +54,33 @@ declare namespace RouterServer {
45
54
  */
46
55
  readonly rpc?: RpcOptions;
47
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
+ };
48
82
  /** The result of {@link render}. */
49
- interface Rendered {
83
+ export interface Rendered {
50
84
  readonly html: string;
51
85
  readonly status: number;
52
86
  }
@@ -56,16 +90,16 @@ declare namespace RouterServer {
56
90
  * with `<!DOCTYPE html>` prepended and the status sourced from the platform
57
91
  * pipeline (200, or 404 for a no-match / page-raised `RouterNotFound`).
58
92
  */
59
- function render(def: RouterDef, options: Options & {
93
+ export function render<R>(def: RouterDef<any, R>, options: Options & {
60
94
  readonly url: string;
61
- }): Effect.Effect<Rendered, Error>;
95
+ } & ContextOption<R>): Effect.Effect<Rendered, Error>;
62
96
  /**
63
97
  * The platform web `fetch`-style handler `(Request) => Promise<Response>` that
64
98
  * dispatches through `HttpApiBuilder` and renders the matched route to
65
99
  * `text/html`. Suitable for bridging into a dev server (e.g. Vite) or any
66
100
  * Web-platform server.
67
101
  */
68
- 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>;
69
103
  /**
70
104
  * Streaming variant of {@link toWebHandler} (spec: "Streaming SSR" in
71
105
  * `router.specs.md`, SW1 … SW7). Same `Options`, memoized separately. Per
@@ -78,7 +112,8 @@ declare namespace RouterServer {
78
112
  * `RouterNotFound` stay real 404s; `POST /_eui/rpc` delegation is unchanged.
79
113
  * `render` and `toWebHandler` remain fully buffered.
80
114
  */
81
- function toStreamingWebHandler(def: RouterDef, options: Options): (request: Request) => Promise<Response>;
115
+ export function toStreamingWebHandler<R>(def: RouterDef<any, R>, options: Options & ContextOption<R>): (request: Request) => Promise<Response>;
116
+ export {};
82
117
  }
83
118
  //#endregion
84
119
  export { RouterServer };
@@ -1 +1 @@
1
- import{l as e,n as t,r as n,s as r}from"../outlet-C7p8KXmO.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{RpcSerialization as y,RpcServer as b,RpcTest as x}from"@effect/rpc";import{SuspenseFailureHandlerTag as S,renderToHydratableShell as C,renderToStringHydratable as w}from"@weftui/dom/server";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){return n.of({currentMatch:_.make({get:u.succeed(e),changes:g.make(e)}),navigate:()=>u.void,httpApiClient:p.none()})}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(x.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 w(u.provideService(e.document({}),n.Outlet,t)).pipe(u.provideService(n,r),u.provide(O(e.rpc)))}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(n,r,i){let a=D(i);return k(r,t(n),a).pipe(u.map(e=>E(e,200)),u.catchIf(e,()=>A(n,r,i.url,404)))}function N(t){return{handle:n=>{let i=l.failureOption(n);return p.isSome(i)&&e(i.value)?p.some({content:t.compiled.notFound(),markNoindex:!0,failureReplay:m.encodeSync(r)(i.value)}):p.none()}}}function P(r,i,a){let o=D(a),s=t(r);return u.gen(function*(){let e=yield*h.make(),{shell:t,patches:a}=yield*C(u.provideService(i.document({}),n.Outlet,s)).pipe(u.provideService(n,o),u.provideService(S,N(r)),u.provide(O(i.rpc)),h.extend(e),u.onError(t=>h.close(e,d.failCause(t)))),l=g.make(`<!DOCTYPE html>\n${t}`).pipe(g.concat(a),g.ensuring(h.close(e,d.void)),g.encodeText);return c.stream(l,{status:200,contentType:`text/html; charset=utf-8`})}).pipe(u.catchIf(e,()=>A(r,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:x}=i.toWebHandler(f.mergeAll(v,s.layerContext)),S=t.rpc,C=x;if(S!==void 0){let{handler:e}=b.toWebHandler(S.group,{layer:f.mergeAll(S.handlers,y.layerJson)});C=t=>new URL(t.url).pathname===`/_eui/rpc`?e(t):x(t)}return c.set(t.document,C),C}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){return u.tryPromise({try:async()=>{let n=await R(e,t)(new Request(z(t.url)));return{html:await n.text(),status:n.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};
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.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Universal nested router for Weft",
5
5
  "license": "MIT",
6
6
  "author": "Stef van Wijchen",
@@ -32,8 +32,8 @@
32
32
  "dependencies": {
33
33
  "@effect/platform": "^0.96.1",
34
34
  "@effect/rpc": "^0.75.1",
35
- "@weftui/core": "0.21.0",
36
- "@weftui/dom": "0.21.0"
35
+ "@weftui/core": "0.22.0",
36
+ "@weftui/dom": "0.22.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/jsdom": "^28.0.3",
@@ -42,7 +42,7 @@
42
42
  "jsdom": "^29.1.1",
43
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"