@weftui/router 0.26.3 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,9 +9,11 @@ Three entry points mirror `@weftui/dom`: `@weftui/router` (authoring + universal
9
9
  ## Installation
10
10
 
11
11
  ```bash
12
- npm install @weftui/core @weftui/dom @weftui/router effect
12
+ npm install @weftui/core @weftui/dom @weftui/router effect@beta
13
13
  ```
14
14
 
15
+ Weft tracks Effect 4's beta line. This release is built and tested against `effect@4.0.0-beta.98`; the peer range accepts newer 4.0 betas, which may contain upstream breaking changes.
16
+
15
17
  `effect` is a peer dependency; `@weftui/core` and `@weftui/dom` provide the tree and renderer.
16
18
 
17
19
  ## Key exports
@@ -1,8 +1,8 @@
1
- import { S as Router, b as NavState, i as RouterDef, m as RouteNode, u as Fields, x as NavigateOptions } from "../compile-CQhAXB5f.js";
2
- import { n as outletNode, r as HrefArgs, t as RouterApp } from "../outlet-CGfLQgZS.js";
1
+ import { S as Router, b as NavState, i as RouterDef, m as RouteNode, u as Fields, x as NavigateOptions } from "../compile-BJFIgBbE.js";
2
+ import { n as outletNode, r as HrefArgs, t as RouterApp } from "../outlet-eoUQ-W0k.js";
3
3
  import { Effect, Layer, Scope } from "effect";
4
4
  import { AppRpcClientTag } from "@weftui/core";
5
- import { RpcGroup } from "@effect/rpc";
5
+ import { RpcGroup } from "effect/unstable/rpc";
6
6
 
7
7
  //#region src/client/router-live.d.ts
8
8
  /**
@@ -63,7 +63,7 @@ interface RouterLiveOptions {
63
63
  * **network** flat rpc client (`RpcClient.make` over `layerProtocolHttp` →
64
64
  * `POST /_eui/rpc`) — so `@weftui/dom` can resolve a `Boundary.rpc` (hydrated
65
65
  * refetch and client-first mount) without depending on this package or
66
- * `@effect/rpc`.
66
+ * `effect/unstable/rpc`.
67
67
  */
68
68
  declare function RouterLive<R>(def: RouterDef<any, R>, options?: RouterLiveOptions & ContextOption<R>): Layer.Layer<Router | AppRpcClientTag | AppServices<R>>;
69
69
  //#endregion
@@ -1,8 +1,9 @@
1
- import { a as Router, i as setResolvedCommit, n as outletNode, o as getPreload, r as preRunLeaf, t as RouterApp } from "../outlet-PoLEThsy.js";
2
- import { r as match, t as href } from "../href-Deoi59OI.js";
3
- import { FetchHttpClient, HttpApiClient } from "@effect/platform";
4
- import { Context, Effect, Exit, Fiber, Layer, Option, Runtime, Schema, Scope, Stream, Subscribable, SubscriptionRef } from "effect";
5
- import { AppRpcClientTag } from "@weftui/core";
1
+ import { a as Router, i as setResolvedCommit, n as outletNode, o as getPreload, r as preRunLeaf, t as RouterApp } from "../outlet-C9N4a4_F.js";
2
+ import { r as match, t as href } from "../href-uvJ6b7zz.js";
3
+ import { HttpApiClient } from "effect/unstable/httpapi";
4
+ import { Context, Effect, Exit, Fiber, Layer, Option, Schema, Scope, Stream, SubscriptionRef } from "effect";
5
+ import { AppRpcClientTag, Subscribable } from "@weftui/core";
6
+ import { FetchHttpClient } from "effect/unstable/http";
6
7
  //#region src/client/link.ts
7
8
  /**
8
9
  * Installs a global, delegated click interceptor (in the `Router` layer scope)
@@ -18,7 +19,7 @@ import { AppRpcClientTag } from "@weftui/core";
18
19
  */
19
20
  function installLinkInterceptor(def, navigate) {
20
21
  return Effect.gen(function* () {
21
- const runtime = yield* Effect.runtime();
22
+ const services = yield* Effect.context();
22
23
  const onClick = (event) => {
23
24
  if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
24
25
  const target = event.target;
@@ -39,7 +40,7 @@ function installLinkInterceptor(def, navigate) {
39
40
  if (to === `${window.location.pathname}${window.location.search}`) return;
40
41
  if (match(def, to)._tag !== "Matched") return;
41
42
  event.preventDefault();
42
- Runtime.runFork(runtime)(navigate(to));
43
+ Effect.runForkWith(services)(navigate(to));
43
44
  };
44
45
  yield* Effect.acquireRelease(Effect.sync(() => document.addEventListener("click", onClick)), () => Effect.sync(() => document.removeEventListener("click", onClick)));
45
46
  });
@@ -73,13 +74,13 @@ function normalizeTo(to) {
73
74
  * **network** flat rpc client (`RpcClient.make` over `layerProtocolHttp` →
74
75
  * `POST /_eui/rpc`) — so `@weftui/dom` can resolve a `Boundary.rpc` (hydrated
75
76
  * refetch and client-first mount) without depending on this package or
76
- * `@effect/rpc`.
77
+ * `effect/unstable/rpc`.
77
78
  */
78
79
  function RouterLive(def, options = {}) {
79
- const core = Layer.scopedContext(Effect.gen(function* () {
80
+ const core = Layer.effectContext(Effect.gen(function* () {
80
81
  const urlRef = yield* SubscriptionRef.make(locationUrl());
81
82
  const navRef = yield* SubscriptionRef.make({ _tag: "Idle" });
82
- const runtime = yield* Effect.runtime();
83
+ const services = yield* Effect.context();
83
84
  const httpApiClient = yield* HttpApiClient.make(def.httpApi, { baseUrl: options.baseUrl ?? window.location.origin }).pipe(Effect.provide(FetchHttpClient.layer));
84
85
  let latest = 0;
85
86
  let inflightPreRun;
@@ -114,11 +115,10 @@ function RouterLive(def, options = {}) {
114
115
  let exit;
115
116
  if (target._tag === "Matched") {
116
117
  const scope = yield* Scope.make();
117
- const fiber = yield* Effect.fork(Effect.provideService(preRunLeaf(router, target), Scope.Scope, scope));
118
+ const fiber = yield* Effect.forkChild(Effect.provideService(preRunLeaf(router, target), Scope.Scope, scope));
118
119
  inflightPreRun = fiber;
119
- if (!emitted) yield* Effect.fork(Effect.gen(function* () {
120
- const done = yield* Fiber.poll(fiber);
121
- if (Option.isNone(done) && token === latest) {
120
+ if (!emitted) yield* Effect.forkChild(Effect.gen(function* () {
121
+ if (fiber.pollUnsafe() === void 0 && token === latest) {
122
122
  emitted = true;
123
123
  yield* SubscriptionRef.set(navRef, {
124
124
  _tag: "Navigating",
@@ -149,28 +149,31 @@ function RouterLive(def, options = {}) {
149
149
  });
150
150
  const navigate = (to, options) => commitTo(normalizeTo(to), true, options?.replace === true);
151
151
  const onPopState = () => {
152
- Runtime.runFork(runtime)(commitTo(locationUrl(), false, false));
152
+ Effect.runForkWith(services)(commitTo(locationUrl(), false, false));
153
153
  };
154
154
  yield* Effect.acquireRelease(Effect.sync(() => window.addEventListener("popstate", onPopState)), () => Effect.sync(() => window.removeEventListener("popstate", onPopState)));
155
155
  yield* installLinkInterceptor(def, navigate);
156
156
  const currentMatch = Subscribable.make({
157
157
  get: Effect.map(SubscriptionRef.get(urlRef), (url) => match(def, url)),
158
- changes: Stream.map(urlRef.changes, (url) => match(def, url))
158
+ changes: Stream.map(SubscriptionRef.changes(urlRef), (url) => match(def, url))
159
159
  });
160
160
  const rpc = options.rpc;
161
161
  let appRpcClient;
162
162
  if (rpc === void 0) appRpcClient = AppRpcClientTag.of({ call: (tag) => Effect.fail(/* @__PURE__ */ new Error(`Boundary.rpc "${tag}" cannot resolve: no \`rpc\` option was passed to RouterLive`)) });
163
163
  else {
164
- const { RpcClient, RpcSerialization } = yield* Effect.promise(() => import("@effect/rpc"));
164
+ const { RpcClient, RpcSerialization } = yield* Effect.promise(() => import("effect/unstable/rpc"));
165
165
  const baseUrl = String(options.baseUrl ?? window.location.origin).replace(/\/$/, "");
166
- const flatClient = yield* RpcClient.make(rpc.group, { flatten: true }).pipe(Effect.provide(RpcClient.layerProtocolHttp({ url: `${baseUrl}${RPC_PATH}` }).pipe(Layer.provide(Layer.mergeAll(FetchHttpClient.layer, RpcSerialization.layerJson)))));
166
+ const flatClient = yield* RpcClient.make(rpc.group, { flatten: true }).pipe(Effect.provide(RpcClient.layerProtocolHttp({ url: `${baseUrl}${RPC_PATH}` }).pipe(Layer.provide(Layer.mergeAll(FetchHttpClient.layer, RpcSerialization.layerNdjson)))));
167
167
  appRpcClient = AppRpcClientTag.of({ call: (tag, payload) => flatClient(tag, payload) });
168
168
  }
169
169
  router = Router.of({
170
170
  currentMatch,
171
171
  navigate,
172
172
  httpApiClient: Option.some(httpApiClient),
173
- navigating: navRef
173
+ navigating: Subscribable.make({
174
+ get: SubscriptionRef.get(navRef),
175
+ changes: SubscriptionRef.changes(navRef)
176
+ })
174
177
  });
175
178
  return Context.make(Router, router).pipe(Context.add(AppRpcClientTag, appRpcClient));
176
179
  }));
@@ -1,20 +1,18 @@
1
- import { HttpApi, HttpApiClient } from "@effect/platform";
2
- import { Context, Effect, Option, Schema, Subscribable } from "effect";
3
- import { Component, Node } from "@weftui/core";
1
+ import { HttpApi, HttpApiClient } from "effect/unstable/httpapi";
2
+ import { Context, Effect, Option, Schema } from "effect";
3
+ import { Component, Node, Subscribable } from "@weftui/core";
4
4
 
5
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
- }>;
6
+ declare const RouterNotFound_base: Schema.Class<RouterNotFound, Schema.TaggedStruct<"RouterNotFound", {
7
+ /** The path that could not be resolved, when known. */readonly path: Schema.optional<Schema.String>;
8
+ }>, import("effect/Cause").YieldableError>;
11
9
  /**
12
10
  * Tagged error raised by {@link notFound} and caught by the router's internal
13
11
  * not-found boundary. Exported so a user can place their own
14
12
  * `Boundary.catchTag("RouterNotFound", …)` to override the fallback for a subtree
15
13
  * (the router's internal boundary is outermost, so a nearer user boundary wins).
16
14
  *
17
- * Modeled as a `Schema.TaggedError` so it can be encoded/decoded across the wire
15
+ * Modeled as a `Schema.TaggedErrorClass` so it can be encoded/decoded across the wire
18
16
  * the same way `Boundary.rpc` replays typed failures.
19
17
  */
20
18
  declare class RouterNotFound extends RouterNotFound_base {}
@@ -29,12 +27,10 @@ declare class RouterNotFound extends RouterNotFound_base {}
29
27
  declare const notFound: (path?: string) => Effect.Effect<never, RouterNotFound>;
30
28
  /** Type guard recognising a {@link RouterNotFound} value regardless of its prototype. */
31
29
  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
- }>;
30
+ declare const RouterParamsError_base: Schema.Class<RouterParamsError, Schema.TaggedStruct<"RouterParamsError", {
31
+ /** Which side of the match failed validation. */readonly source: Schema.Literals<readonly ["path", "query"]>; /** The requested field names, for diagnostics. */
32
+ readonly keys: Schema.$Array<Schema.String>;
33
+ }>, import("effect/Cause").YieldableError>;
38
34
  /**
39
35
  * Tagged error raised by `Router.params` / `Router.query` when the live match does
40
36
  * not satisfy the requested fields — either no route is matched, or a requested
@@ -45,7 +41,7 @@ declare const RouterParamsError_base: Schema.TaggedErrorClass<RouterParamsError,
45
41
  * It bubbles up through the route tree's aggregate error channel, so a user may
46
42
  * place a `Boundary.catchTag("RouterParamsError", …)` to recover within a subtree.
47
43
  *
48
- * Modeled as a `Schema.TaggedError` so it can be encoded/decoded across the wire
44
+ * Modeled as a `Schema.TaggedErrorClass` so it can be encoded/decoded across the wire
49
45
  * the same way `RouterNotFound` and `Boundary.rpc` replay typed failures.
50
46
  */
51
47
  declare class RouterParamsError extends RouterParamsError_base {}
@@ -63,16 +59,16 @@ type RouteMatch = {
63
59
  readonly url: string;
64
60
  };
65
61
  /** A string-encodeable schema as carried by an HttpApi endpoint's path/urlParams slot. */
66
- type ParamSchema = Schema.Schema<Record<string, unknown>, unknown, never>;
62
+ type ParamSchema = Schema.Codec<Record<string, unknown>, unknown, never>;
67
63
  /** A precompiled regex + decode schemas for one leaf, sourced from its HttpApi endpoint. */
68
64
  interface MatcherEntry {
69
65
  /** The compiled leaf (render/nesting metadata), resolved from the endpoint id. */
70
66
  readonly leaf: CompiledLeaf;
71
67
  readonly regex: RegExp;
72
68
  readonly paramNames: readonly string[];
73
- /** Path-param schema, read from the endpoint's `setPath` slot. */
69
+ /** Path-param schema, read from the endpoint's `params` slot. */
74
70
  readonly pathSchema: ParamSchema;
75
- /** Query schema, read from the endpoint's `setUrlParams` slot. */
71
+ /** Query schema, read from the endpoint's `query` slot. */
76
72
  readonly querySchema: ParamSchema;
77
73
  }
78
74
  /**
@@ -115,7 +111,7 @@ declare function match(def: RouterDef, url: string): RouteMatch;
115
111
  */
116
112
  /**
117
113
  * The platform `HttpApiClient` derived from a router's `HttpApi` spine. Typed
118
- * opaquely (`Client<any, …>`) because the spine is `HttpApi.Any` — its
114
+ * opaquely (`Client<any, …>`) because the spine is `HttpApi.Top` — its
119
115
  * group/endpoint shapes are assembled in a runtime loop by `buildHttpApi`, so a
120
116
  * precise client type is not recoverable. Present (`Option.some`) on the client
121
117
  * (`RouterLive`), absent (`Option.none`) on the server, which is itself the origin.
@@ -145,7 +141,7 @@ interface NavigateOptions {
145
141
  */
146
142
  readonly replace?: boolean;
147
143
  }
148
- declare const Router_base: Context.TagClass<Router, "@weftui/router/Router", {
144
+ declare const Router_base: Context.ServiceClass<Router, "@weftui/router/Router", {
149
145
  /** The current match as a hot `Subscribable`; drives the outlet. */readonly currentMatch: Subscribable.Subscribable<RouteMatch>;
150
146
  /**
151
147
  * Navigates to `to` (a path, optionally with a query). On the client this
@@ -170,7 +166,7 @@ declare const Router_base: Context.TagClass<Router, "@weftui/router/Router", {
170
166
  readonly navigating: Subscribable.Subscribable<NavState>;
171
167
  }>;
172
168
  declare class Router extends Router_base {}
173
- declare const OutletTag_base: Context.TagClass<OutletTag, "@weftui/router/Outlet", Node<never, never>>;
169
+ declare const OutletTag_base: Context.ServiceClass<OutletTag, "@weftui/router/Outlet", Node<never, never>>;
174
170
  /**
175
171
  * The injected outlet: the node a layout (or the server document shell) splices
176
172
  * to place the next level down. Provided per render by the router
@@ -456,16 +452,16 @@ interface CompiledLeaf {
456
452
  /**
457
453
  * Path-param schema. Its **encoded** side is typed string-encodeable
458
454
  * (`Record<string, string | undefined>`) so it satisfies platform's
459
- * `HttpApiEndpoint.setPath` constraint without an `as any` cast — param schemas
455
+ * `HttpApiEndpoint` `params` constraint without an `as any` cast — param schemas
460
456
  * round-trip strings, so the `Schema.Struct` value is asserted to this shape.
461
457
  */
462
- readonly pathSchema: Schema.Schema<Record<string, unknown>, Readonly<Record<string, string | undefined>>>;
458
+ readonly pathSchema: Schema.Codec<Record<string, unknown>, Readonly<Record<string, string | undefined>>>;
463
459
  /**
464
460
  * Query schema. Its **encoded** side is typed string-encodeable
465
461
  * (`Record<string, string | ReadonlyArray<string> | undefined>`) so it satisfies
466
- * platform's `HttpApiEndpoint.setUrlParams` constraint without a cast.
462
+ * platform's `HttpApiEndpoint` `query` constraint without a cast.
467
463
  */
468
- readonly querySchema: Schema.Schema<Record<string, unknown>, Readonly<Record<string, string | ReadonlyArray<string> | undefined>>>;
464
+ readonly querySchema: Schema.Codec<Record<string, unknown>, Readonly<Record<string, string | ReadonlyArray<string> | undefined>>>;
469
465
  /** The page's component slot; invoked per render, reads params via `Router.params` / `Router.query`. */
470
466
  readonly component: ComponentSlot;
471
467
  /** Ancestor layouts (root → parent) wrapping this leaf. */
@@ -493,7 +489,7 @@ interface RouterDef<E = any, R = any> {
493
489
  * nesting/render metadata platform's flat API can't represent. Built by
494
490
  * {@link buildHttpApi} during {@link makeRouter}.
495
491
  */
496
- readonly httpApi: HttpApi.HttpApi.Any;
492
+ readonly httpApi: HttpApi.Top;
497
493
  /**
498
494
  * Phantom marker for the tree's aggregate error channel. Covariant (stores `E`
499
495
  * directly) so a fully-static `RouterDef<never, never>` stays assignable to the
@@ -531,20 +527,20 @@ declare function compile(def: {
531
527
  /**
532
528
  * Builds the authoritative `HttpApi` for a compiled tree (S4): a single `"pages"`
533
529
  * group whose endpoints are GET endpoints — one per leaf — at each leaf's full path
534
- * pattern, carrying `setPath(pathSchema)`, `setUrlParams(querySchema)`, a
530
+ * pattern, carrying `params: pathSchema`, `query: querySchema`, a
535
531
  * `Schema.String` (text/HTML) success, and a `RouterNotFound → 404` error. The tree
536
532
  * (not `HttpApi`) is the authoring surface; this is the single source of truth the
537
533
  * server dispatch (`HttpApiBuilder`) and the client matcher / derived `HttpApiClient`
538
534
  * read from, so both sides agree on paths and schemas.
539
535
  *
540
536
  * Each leaf's `pathSchema`/`querySchema` are typed string-encodeable (see
541
- * {@link CompiledLeaf}), so `setPath`/`setUrlParams` need no `as any` casts.
537
+ * {@link CompiledLeaf}), so the `params`/`query` options need no `as any` casts.
542
538
  *
543
539
  * `Boundary.rpc` data no longer rides this spine: it resolves through the app's
544
540
  * merged `RpcGroup` over the ambient `AppRpcClient` (`POST /_eui/rpc`), wired
545
541
  * explicitly into `RouterServer`/`RouterLive`. The matcher reads only `"pages"`.
546
542
  */
547
- declare function buildHttpApi(leaves: readonly CompiledLeaf[]): HttpApi.HttpApi.Any;
543
+ declare function buildHttpApi(leaves: readonly CompiledLeaf[]): HttpApi.Top;
548
544
  /**
549
545
  * Seals a route tree into a {@link RouterDef}, compiling it eagerly (so leaf
550
546
  * references are stamped for `href`), building its authoritative {@link buildHttpApi}
@@ -1,5 +1,5 @@
1
- import { l as leafRegistry } from "./outlet-PoLEThsy.js";
2
- import { Either, Option, Schema } from "effect";
1
+ import { l as leafRegistry } from "./outlet-C9N4a4_F.js";
2
+ import { Result, Schema } from "effect";
3
3
  //#region src/matcher.ts
4
4
  /** Escapes a literal path segment for inclusion in a `RegExp`. */
5
5
  function escapeRegex(literal) {
@@ -50,14 +50,14 @@ function compileMatchers(def) {
50
50
  const endpoints = def.httpApi.groups["pages"]?.endpoints ?? {};
51
51
  const entries = [];
52
52
  for (const endpoint of Object.values(endpoints)) {
53
- const leaf = leafById.get(endpoint.name);
53
+ const leaf = leafById.get(endpoint.identifier);
54
54
  if (leaf === void 0) continue;
55
55
  entries.push({
56
56
  leaf,
57
57
  regex: patternToRegex(endpoint.path),
58
58
  paramNames: paramNamesOf(endpoint.path),
59
- pathSchema: Option.getOrElse(endpoint.pathSchema, () => emptySchema),
60
- querySchema: Option.getOrElse(endpoint.urlParamsSchema, () => emptySchema)
59
+ pathSchema: endpoint.params ?? emptySchema,
60
+ querySchema: endpoint.query ?? emptySchema
61
61
  });
62
62
  }
63
63
  entries.sort((a, b) => {
@@ -108,15 +108,15 @@ function match(def, url) {
108
108
  const raw = m[i + 1];
109
109
  if (raw !== void 0) rawParams[name] = decodeURIComponent(raw);
110
110
  });
111
- const decodedPath = Schema.decodeUnknownEither(entry.pathSchema)(rawParams);
112
- if (Either.isLeft(decodedPath)) continue;
113
- const decodedQuery = Schema.decodeUnknownEither(entry.querySchema)(parseQuery(search));
114
- if (Either.isLeft(decodedQuery)) continue;
111
+ const decodedPath = Schema.decodeUnknownResult(entry.pathSchema)(rawParams);
112
+ if (Result.isFailure(decodedPath)) continue;
113
+ const decodedQuery = Schema.decodeUnknownResult(entry.querySchema)(parseQuery(search));
114
+ if (Result.isFailure(decodedQuery)) continue;
115
115
  return {
116
116
  _tag: "Matched",
117
117
  leaf: entry.leaf,
118
- path: decodedPath.right,
119
- query: decodedQuery.right,
118
+ path: decodedPath.success,
119
+ query: decodedQuery.success,
120
120
  url: normalizedUrl
121
121
  };
122
122
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { A as notFound, C as RouterHttpApiClient, D as RouterNotFound, E as match, O as RouterParamsError, S as Router, T as compileMatchers, _ as TreeE, a as RouterOptions, b as NavState, c as leafRegistry, d as FieldsType, f as LayoutNode, g as SubtreeR, h as SubtreeE, i as RouterDef, k as isRouterNotFound, l as ComponentSlot, m as RouteNode, n as CompiledLayout, o as buildHttpApi, p as RouteHandlerProps, r as CompiledLeaf, s as compile, t as Compiled, u as Fields, v as TreeNode, w as RouteMatch, x as NavigateOptions, y as TreeR } from "./compile-CQhAXB5f.js";
2
- import { i as href, n as outletNode, r as HrefArgs, t as RouterApp } from "./outlet-CGfLQgZS.js";
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-BJFIgBbE.js";
2
+ import { i as href, n as outletNode, r as HrefArgs, t as RouterApp } from "./outlet-eoUQ-W0k.js";
3
3
  export { type Compiled, type CompiledLayout, type CompiledLeaf, type ComponentSlot, type Fields, type FieldsType, type HrefArgs, type LayoutNode, type NavState, type NavigateOptions, type RouteHandlerProps, type RouteMatch, type RouteNode, Router, RouterApp, type RouterDef, type RouterHttpApiClient, RouterNotFound, type RouterOptions, RouterParamsError, type SubtreeE, type SubtreeR, type TreeE, type TreeNode, type TreeR, buildHttpApi, compile, compileMatchers, href, isRouterNotFound, leafRegistry, match, notFound, outletNode };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as Router, c as compile, d as RouterParamsError, f as isRouterNotFound, l as leafRegistry, n as outletNode, p as notFound, s as buildHttpApi, t as RouterApp, u as RouterNotFound } from "./outlet-PoLEThsy.js";
2
- import { n as compileMatchers, r as match, t as href } from "./href-Deoi59OI.js";
1
+ import { a as Router, c as compile, d as RouterParamsError, f as isRouterNotFound, l as leafRegistry, n as outletNode, p as notFound, s as buildHttpApi, t as RouterApp, u as RouterNotFound } from "./outlet-C9N4a4_F.js";
2
+ import { n as compileMatchers, r as match, t as href } from "./href-uvJ6b7zz.js";
3
3
  export { Router, RouterApp, RouterNotFound, RouterParamsError, buildHttpApi, compile, compileMatchers, href, isRouterNotFound, leafRegistry, match, notFound, outletNode };
@@ -1,6 +1,6 @@
1
- import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform";
2
- import { Context, Effect, Exit, Schema, Stream, Subscribable, pipe } from "effect";
3
- import { Boundary, getElementDescriptor, h } from "@weftui/core";
1
+ import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiSchema } from "effect/unstable/httpapi";
2
+ import { Context, Effect, Exit, Schema, Stream, pipe } from "effect";
3
+ import { Boundary, Subscribable, getElementDescriptor, h } from "@weftui/core";
4
4
  //#region src/errors.ts
5
5
  /**
6
6
  * Tagged error raised by {@link notFound} and caught by the router's internal
@@ -8,10 +8,10 @@ import { Boundary, getElementDescriptor, h } from "@weftui/core";
8
8
  * `Boundary.catchTag("RouterNotFound", …)` to override the fallback for a subtree
9
9
  * (the router's internal boundary is outermost, so a nearer user boundary wins).
10
10
  *
11
- * Modeled as a `Schema.TaggedError` so it can be encoded/decoded across the wire
11
+ * Modeled as a `Schema.TaggedErrorClass` so it can be encoded/decoded across the wire
12
12
  * the same way `Boundary.rpc` replays typed failures.
13
13
  */
14
- var RouterNotFound = class extends Schema.TaggedError()("RouterNotFound", {
14
+ var RouterNotFound = class extends Schema.TaggedErrorClass()("RouterNotFound", {
15
15
  /** The path that could not be resolved, when known. */
16
16
  path: Schema.optional(Schema.String) }) {};
17
17
  /**
@@ -35,12 +35,12 @@ const isRouterNotFound = (u) => typeof u === "object" && u !== null && "_tag" in
35
35
  * It bubbles up through the route tree's aggregate error channel, so a user may
36
36
  * place a `Boundary.catchTag("RouterParamsError", …)` to recover within a subtree.
37
37
  *
38
- * Modeled as a `Schema.TaggedError` so it can be encoded/decoded across the wire
38
+ * Modeled as a `Schema.TaggedErrorClass` so it can be encoded/decoded across the wire
39
39
  * the same way `RouterNotFound` and `Boundary.rpc` replay typed failures.
40
40
  */
41
- var RouterParamsError = class extends Schema.TaggedError()("RouterParamsError", {
41
+ var RouterParamsError = class extends Schema.TaggedErrorClass()("RouterParamsError", {
42
42
  /** Which side of the match failed validation. */
43
- source: Schema.Literal("path", "query"),
43
+ source: Schema.Literals(["path", "query"]),
44
44
  /** The requested field names, for diagnostics. */
45
45
  keys: Schema.Array(Schema.String)
46
46
  }) {};
@@ -157,21 +157,26 @@ function compile(def) {
157
157
  /**
158
158
  * Builds the authoritative `HttpApi` for a compiled tree (S4): a single `"pages"`
159
159
  * group whose endpoints are GET endpoints — one per leaf — at each leaf's full path
160
- * pattern, carrying `setPath(pathSchema)`, `setUrlParams(querySchema)`, a
160
+ * pattern, carrying `params: pathSchema`, `query: querySchema`, a
161
161
  * `Schema.String` (text/HTML) success, and a `RouterNotFound → 404` error. The tree
162
162
  * (not `HttpApi`) is the authoring surface; this is the single source of truth the
163
163
  * server dispatch (`HttpApiBuilder`) and the client matcher / derived `HttpApiClient`
164
164
  * read from, so both sides agree on paths and schemas.
165
165
  *
166
166
  * Each leaf's `pathSchema`/`querySchema` are typed string-encodeable (see
167
- * {@link CompiledLeaf}), so `setPath`/`setUrlParams` need no `as any` casts.
167
+ * {@link CompiledLeaf}), so the `params`/`query` options need no `as any` casts.
168
168
  *
169
169
  * `Boundary.rpc` data no longer rides this spine: it resolves through the app's
170
170
  * merged `RpcGroup` over the ambient `AppRpcClient` (`POST /_eui/rpc`), wired
171
171
  * explicitly into `RouterServer`/`RouterLive`. The matcher reads only `"pages"`.
172
172
  */
173
173
  function buildHttpApi(leaves) {
174
- const group = leaves.reduce((g, leaf) => g.add(HttpApiEndpoint.get(leaf.id, leaf.fullPathPattern).setPath(leaf.pathSchema).setUrlParams(leaf.querySchema).addSuccess(Schema.String).addError(RouterNotFound, { status: 404 })), HttpApiGroup.make("pages"));
174
+ const group = leaves.reduce((g, leaf) => g.add(HttpApiEndpoint.get(leaf.id, leaf.fullPathPattern, {
175
+ params: leaf.pathSchema,
176
+ query: leaf.querySchema,
177
+ success: Schema.String,
178
+ error: RouterNotFound.pipe(HttpApiSchema.status(404))
179
+ })), HttpApiGroup.make("pages"));
175
180
  return HttpApi.make("router").add(group);
176
181
  }
177
182
  /**
@@ -283,7 +288,7 @@ function lazyComponent(load) {
283
288
  }
284
289
  //#endregion
285
290
  //#region src/router-service.ts
286
- var Router = class extends Context.Tag("@weftui/router/Router")() {};
291
+ var Router = class extends Context.Service()("@weftui/router/Router") {};
287
292
  /**
288
293
  * The injected outlet: the node a layout (or the server document shell) splices
289
294
  * to place the next level down. Provided per render by the router
@@ -295,7 +300,7 @@ var Router = class extends Context.Tag("@weftui/router/Router")() {};
295
300
  * structurally by {@link makeLayout} / {@link makeRouter}, never inferred across
296
301
  * this DI boundary. Re-exported on the namespace as `Router.Outlet`.
297
302
  */
298
- var OutletTag = class extends Context.Tag("@weftui/router/Outlet")() {};
303
+ var OutletTag = class extends Context.Service()("@weftui/router/Outlet") {};
299
304
  /** Picks the requested `fields` keys out of a decoded match record. */
300
305
  function pick(fields, record) {
301
306
  const subset = {};
@@ -1,4 +1,4 @@
1
- import { D as RouterNotFound, S as Router, d as FieldsType, i as RouterDef, m as RouteNode, u as Fields } from "./compile-CQhAXB5f.js";
1
+ import { D as RouterNotFound, S as Router, d as FieldsType, i as RouterDef, m as RouteNode, u as Fields } from "./compile-BJFIgBbE.js";
2
2
  import { Node } from "@weftui/core";
3
3
 
4
4
  //#region src/href.d.ts
@@ -1,7 +1,7 @@
1
- import { S as Router, i as RouterDef, l as ComponentSlot } from "../compile-CQhAXB5f.js";
1
+ import { S as Router, i as RouterDef, l as ComponentSlot } from "../compile-BJFIgBbE.js";
2
2
  import { Effect, Layer } from "effect";
3
3
  import { AppRpcClientTag } from "@weftui/core";
4
- import { RpcGroup } from "@effect/rpc";
4
+ import { RpcGroup } from "effect/unstable/rpc";
5
5
 
6
6
  //#region src/server/router-server.d.ts
7
7
  /**
@@ -1,9 +1,10 @@
1
- import { a as Router, f as isRouterNotFound, n as outletNode, u as RouterNotFound } from "../outlet-PoLEThsy.js";
2
- import { HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpServer, HttpServerResponse } from "@effect/platform";
3
- import { Cause, Effect, Exit, Layer, Option, Schema, Scope, Stream, Subscribable } from "effect";
4
- import { AppRpcClientTag } from "@weftui/core";
1
+ import { a as Router, f as isRouterNotFound, n as outletNode, u as RouterNotFound } from "../outlet-C9N4a4_F.js";
2
+ import { HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
3
+ import { Cause, Effect, Exit, Layer, Option, Schema, Scope, Stream } from "effect";
4
+ import { AppRpcClientTag, Subscribable } from "@weftui/core";
5
+ import { HttpRouter, HttpServer, HttpServerResponse } from "effect/unstable/http";
5
6
  import { SuspenseFailureHandlerTag, renderToHydratableShell, renderToStringHydratable } from "@weftui/dom/server";
6
- import { RpcSerialization, RpcServer, RpcTest } from "@effect/rpc";
7
+ import { RpcSerialization, RpcServer, RpcTest } from "effect/unstable/rpc";
7
8
  //#region src/server/router-server.ts
8
9
  let RouterServer;
9
10
  (function(_RouterServer) {
@@ -44,7 +45,7 @@ let RouterServer;
44
45
  */
45
46
  function appRpcClientLayer(rpc) {
46
47
  if (rpc === void 0) return Layer.succeed(AppRpcClientTag, AppRpcClientTag.of({ call: (tag) => Effect.fail(/* @__PURE__ */ new Error(`Boundary.rpc "${tag}" cannot resolve: no \`rpc\` option was passed to RouterServer`)) }));
47
- return Layer.scoped(AppRpcClientTag, Effect.map(RpcTest.makeClient(rpc.group, { flatten: true }), (flat) => AppRpcClientTag.of({ call: (tag, payload) => flat(tag, payload) }))).pipe(Layer.provide(rpc.handlers));
48
+ return Layer.effect(AppRpcClientTag, Effect.map(RpcTest.makeClient(rpc.group, { flatten: true }), (flat) => AppRpcClientTag.of({ call: (tag, payload) => flat(tag, payload) }))).pipe(Layer.provide(rpc.handlers));
48
49
  }
49
50
  /**
50
51
  * Renders the document shell — with `app` spliced via `Router.Outlet` — to a
@@ -106,7 +107,7 @@ let RouterServer;
106
107
  */
107
108
  function notFoundSuspenseHandler(def) {
108
109
  return { handle: (cause) => {
109
- const failure = Cause.failureOption(cause);
110
+ const failure = Cause.findErrorOption(cause);
110
111
  return Option.isSome(failure) && isRouterNotFound(failure.value) ? Option.some({
111
112
  content: def.compiled.notFound(),
112
113
  markNoindex: true,
@@ -126,7 +127,7 @@ let RouterServer;
126
127
  const app = outletNode(def);
127
128
  return Effect.gen(function* () {
128
129
  const scope = yield* Scope.make();
129
- const { shell, patches } = yield* renderToHydratableShell(Effect.provideService(options.document({}), Router.Outlet, app)).pipe(Effect.provideService(Router, router), Effect.provideService(SuspenseFailureHandlerTag, notFoundSuspenseHandler(def)), Effect.provide(appRpcClientLayer(options.rpc)), Effect.provide(options.context ?? Layer.empty), Scope.extend(scope), Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))));
130
+ const { shell, patches } = yield* renderToHydratableShell(Effect.provideService(options.document({}), Router.Outlet, app)).pipe(Effect.provideService(Router, router), Effect.provideService(SuspenseFailureHandlerTag, notFoundSuspenseHandler(def)), Effect.provide(appRpcClientLayer(options.rpc)), Effect.provide(options.context ?? Layer.empty), Scope.provide(scope), Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))));
130
131
  const body = Stream.make(`<!DOCTYPE html>\n${shell}`).pipe(Stream.concat(patches), Stream.ensuring(Scope.close(scope, Exit.void)), Stream.encodeText);
131
132
  return HttpServerResponse.stream(body, {
132
133
  status: 200,
@@ -155,24 +156,24 @@ let RouterServer;
155
156
  if (cached !== void 0) return cached;
156
157
  const leaves = def.compiled.leaves;
157
158
  const builder = HttpApiBuilder;
158
- const fallbackGroup = HttpApiGroup.make("fallback").add(HttpApiEndpoint.get("catchAll", "*").addSuccess(Schema.String));
159
+ const fallbackGroup = HttpApiGroup.make("fallback").add(HttpApiEndpoint.get("catchAll", "*", { success: Schema.String }));
159
160
  const api = def.httpApi.add(fallbackGroup);
160
161
  const pagesLayer = builder.group(api, "pages", (handlers) => leaves.reduce((h, leaf) => h.handle(leaf.id, (request) => leafRenderer(def, options, {
161
162
  _tag: "Matched",
162
163
  leaf,
163
- path: request.path,
164
- query: request.urlParams,
164
+ path: request.params,
165
+ query: request.query,
165
166
  url: request.request.url
166
167
  })), handlers));
167
168
  const fallbackLayer = builder.group(api, "fallback", (handlers) => handlers.handle("catchAll", (request) => renderNoMatch(def, options, request.request.url)));
168
- const apiLayer = builder.api(api).pipe(Layer.provide(Layer.mergeAll(pagesLayer, fallbackLayer)));
169
- const { handler: pageHandler } = HttpApiBuilder.toWebHandler(Layer.mergeAll(apiLayer, HttpServer.layerContext));
169
+ const apiRoutes = HttpApiBuilder.layer(api).pipe(Layer.provide(Layer.mergeAll(pagesLayer, fallbackLayer)));
170
170
  const rpc = options.rpc;
171
- let handler = pageHandler;
172
- if (rpc !== void 0) {
173
- const { handler: rpcHandler } = RpcServer.toWebHandler(rpc.group, { layer: Layer.mergeAll(rpc.handlers, RpcSerialization.layerJson) });
174
- handler = (request) => new URL(request.url).pathname === RPC_PATH ? rpcHandler(request) : pageHandler(request);
175
- }
171
+ const rpcRoutes = rpc !== void 0 ? RpcServer.layerHttp({
172
+ group: rpc.group,
173
+ path: RPC_PATH,
174
+ protocol: "http"
175
+ }).pipe(Layer.provide(Layer.mergeAll(rpc.handlers, RpcSerialization.layerNdjson))) : Layer.empty;
176
+ const { handler } = HttpRouter.toWebHandler(Layer.mergeAll(apiRoutes, rpcRoutes, HttpServer.layerServices));
176
177
  perDef.set(options.document, handler);
177
178
  return handler;
178
179
  }
@@ -18,29 +18,29 @@ A component's `E` channel accumulates up the tree. A **failure boundary** is whe
18
18
  ```typescript
19
19
  import { Boundary, h } from "@weftui/core";
20
20
 
21
- Boundary.catchAll({ fallback: (e) => h.div({ class: "error" }, `Failed: ${e.message}`) }, [
21
+ Boundary.catch({ fallback: (e) => h.div({ class: "error" }, `Failed: ${e.message}`) }, [
22
22
  RiskyWidget(),
23
23
  ]);
24
24
  ```
25
25
 
26
26
  There are six failure-catch variants, mirroring Effect's own error operators so the mental model transfers directly:
27
27
 
28
- | Variant | Catches |
29
- | ------------------------ | ------------------------------------------ |
30
- | `catchAll` | every failure in `E` |
31
- | `catchAllCause` | the full `Cause` (defects included) |
32
- | `catchTag` / `catchTags` | one / several tagged errors by `_tag` |
33
- | `catchSome` / `catchIf` | a selected subset, by `Option` / predicate |
28
+ | Variant | Catches |
29
+ | ------------------------- | ------------------------------------------ |
30
+ | `catch` | every failure in `E` |
31
+ | `catchCause` | the full `Cause` (defects included) |
32
+ | `catchTag` / `catchTags` | one / several tagged errors by `_tag` |
33
+ | `catchFilter` / `catchIf` | a selected subset, by `Filter` / predicate |
34
34
 
35
- The channel algebra is the whole reason they exist: `catchTag("Foo", …)` removes `Foo` from the children's `E` and adds whatever the fallback needs — so the type of the boundary node reflects exactly which failures are still live and which were handled. An unhandled failure re-raises to the **nearest enclosing** boundary; if none catches it at **mount time**, mounting fails. Boundaries nest, so an inner `catchTag` can handle a specific case while an outer `catchAll` sweeps the rest.
35
+ The channel algebra is the whole reason they exist: `catchTag("Foo", …)` removes `Foo` from the children's `E` and adds whatever the fallback needs — so the type of the boundary node reflects exactly which failures are still live and which were handled. An unhandled failure re-raises to the **nearest enclosing** boundary; if none catches it at **mount time**, mounting fails. Boundaries nest, so an inner `catchTag` can handle a specific case while an outer `catch` sweeps the rest.
36
36
 
37
37
  ### Post-mount failures with no enclosing boundary
38
38
 
39
39
  The routing above describes what happens while a node is being built. Once mounted, a reactive region — an attribute, child, or list stream, or a hydrated equivalent — keeps running for the lifetime of its scope, and it can still fail later: a `Stream` backing a `Boundary.rpc` resource might raise `RouterNotFound` after a client-side navigation, for instance. If a `BoundaryContext` encloses the region, the failure routes to it exactly as above, and the boundary's fallback swaps in.
40
40
 
41
- If no boundary encloses it, there is nothing to swap to. Weft does not synthesize one: the region's DOM keeps its last rendered content, and the subscription fiber's failure exit is left **unobserved**. The Effect runtime itself then reports it `"Fiber terminated with an unhandled error"` because Weft raises that fiber's `FiberRef.unhandledErrorLogLevel` from the ambient default (`Debug`) to `LogLevel.Error` and annotates the log with `weft.region`, identifying the failing region by kind and identity (e.g. `attribute:class`, `child:stream-3`, `list:stream-2`, `hydrate:stream-1 (/products/42)`). This fires for typed failures and defects alike, in both dev and prod, exactly once per failing region. Interruption — the ordinary case of unmount tearing down the region's scope — is never reported; only genuine failures are.
41
+ If no boundary encloses it, there is nothing to swap to. Weft does not synthesize one: the region's DOM keeps its last rendered content, and a watcher fiber forked into the same scope alongside the subscription itself observes its exit directly. When that exit is a failure whose cause is not interruption-only, Weft reports it explicitly via `Effect.logError(exit.cause)`, annotated with `weft.region` to identify the failing region by kind and identity (e.g. `attribute:class`, `child:stream-3`, `list:stream-2`, `hydrate:stream-1 (/products/42)`). This fires for typed failures and defects alike, in both dev and prod, exactly once per failing region, at the `"Error"` level. Interruption — the ordinary case of unmount tearing down the region's scope — is never reported; only genuine failures are.
42
42
 
43
- This is deliberate: rather than a Weft-specific error-reporting config, visibility is controlled by the same knobs any Effect program uses — `Logger.withMinimumLogLevel` to filter it, `Effect.withUnhandledErrorLogLevel` to change how loudly (or quietly) unhandled fiber exits are reported elsewhere in your program. A stream that can fail and has no enclosing boundary is a stream whose failures you've chosen not to route into the UI — the log is what tells you that decision has consequences at runtime.
43
+ This is deliberate: rather than leaving the failure to whatever the Effect runtime would otherwise do with an unobserved fiber exit, Weft observes and logs it itself, so visibility is controlled by the same knobs any Effect program uses — `References.MinimumLogLevel` (provided via `Effect.provideService`) to filter it, or a custom `Logger` to route it elsewhere. A stream that can fail and has no enclosing boundary is a stream whose failures you've chosen not to route into the UI — the log is what tells you that decision has consequences at runtime.
44
44
 
45
45
  ## Suspense boundaries
46
46