@weftui/dom 0.27.0 → 0.28.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.
@@ -2,7 +2,7 @@
2
2
  title: "@weftui/dom"
3
3
  order: 2
4
4
  section: reference
5
- description: Full API surface for @weftui/dom — the client renderer (mount, hydrate) and the server renderer (renderToString and streaming variants).
5
+ description: Full API surface for @weftui/dom — the WeftApp client runtime (make, mount, hydrate, errors, dispose) and the server renderer (renderToString and streaming variants).
6
6
  ---
7
7
 
8
8
  # @weftui/dom API Reference
@@ -14,70 +14,191 @@ for a narrative walkthrough.
14
14
 
15
15
  ## `@weftui/dom/client`
16
16
 
17
- ### `mount`
17
+ `WeftApp` is the client entry point's app namespace (`export * as WeftApp from
18
+ "./weft-app"`). One `WeftApp` value is one lazily-built `ManagedRuntime` (the app
19
+ layer) + one root `Scope` + one unhandled-error hub. Each `WeftApp.mount` /
20
+ `WeftApp.hydrate` call creates a child **root scope** under the app scope;
21
+ layer-built services are shared **by reference** across every root mounted from the
22
+ same app (layer memoization), which is what makes cross-island reactive state work
23
+ (see [examples/shared-state-islands](https://github.com/stefvw93/weft/tree/main/examples/shared-state-islands)). The
24
+ barrel also re-exports `MountError`, `HydrateError`, `RootHandle`, `UnhandledError`,
25
+ and the `WeftApp` interface's type as `WeftAppType` (renamed on export to avoid
26
+ colliding with the `WeftApp` namespace import).
27
+
28
+ ### `WeftApp.make`
29
+
30
+ ```ts
31
+ const make: {
32
+ (): WeftApp<never, never>;
33
+ <R, E>(
34
+ layer: Layer.Layer<R, E, never>,
35
+ options?: { readonly memoMap?: Layer.MemoMap },
36
+ ): WeftApp<R, E>;
37
+ };
38
+ ```
39
+
40
+ Creates a `WeftApp` from an app layer. Synchronous and side-effect-free with
41
+ respect to the layer: the layer builds **lazily** on the first `mount` / `hydrate`
42
+ (or the first direct `app.runtime` run) — `ManagedRuntime.make` semantics. A layer
43
+ whose construction has an observable side effect shows that effect only after the
44
+ first mount, never at `make` time. `options.memoMap` shares layer memoization
45
+ across multiple `WeftApp` instances.
46
+
47
+ There is deliberately no `makeScoped`. To bind an app's lifetime to a scope,
48
+ compose it yourself:
49
+
50
+ ```ts
51
+ const acquireApp = Effect.acquireRelease(
52
+ Effect.sync(() => WeftApp.make(AppLive)),
53
+ (app) => WeftApp.dispose(app),
54
+ );
55
+ ```
56
+
57
+ ### `WeftApp.mount`
58
+
59
+ ```ts
60
+ const mount: <R, E>(
61
+ app: WeftApp<R, E>,
62
+ node: Renderable,
63
+ root: HTMLElement,
64
+ ) => Effect.Effect<RootHandle, E | MountError>;
65
+ ```
66
+
67
+ Mounts `node` into `root` as a new root of `app`. Self-contained — the returned
68
+ effect's requirement channel is `never`, so it runs with a bare `Effect.runPromise`;
69
+ services come exclusively from the app layer, and an `Effect.provide` wrapped around
70
+ this call does not reach components. Clears `root`'s existing children, renders,
71
+ appends the result. Completes after initial render; streams keep running in the
72
+ background, owned by the root's scope (a child of the app scope). The app layer
73
+ builds lazily here on first mount; its error channel `E` surfaces at that point. On
74
+ render failure the root scope is closed before the error propagates; the app
75
+ runtime and other roots are untouched. Mounting on a disposed app fails — it does
76
+ not hang.
77
+
78
+ ### `WeftApp.hydrate`
79
+
80
+ ```ts
81
+ function hydrate<A extends Renderable, R = never, E = never>(
82
+ app: WeftApp<R, E>,
83
+ node: A,
84
+ root: HTMLElement,
85
+ ): [AssertNoServerOnly<CoreNode.Context<A>>] extends [CoreNode.Context<A>]
86
+ ? Effect.Effect<RootHandle, E | HydrateError>
87
+ : ServerOnlyLeak;
88
+ ```
89
+
90
+ Continues, on the client, the DOM produced on the server by
91
+ `renderToStringHydratable` / `renderToStreamHydratable`, as a new root of `app`.
92
+ Unlike `mount`, does **not** clear `root`: it walks the node tree in lockstep with
93
+ the existing server DOM, adopting nodes in place. Error channel is `E |
94
+ HydrateError` (adds `HydrationMismatchError` on top of everything `mount` can fail
95
+ with). Preserves the compile-time `AssertNoServerOnly` → `ServerOnlyLeak` guard: a
96
+ server-only requirement left in `node`'s context degrades the return type to the
97
+ `ServerOnlyLeak` sentinel (compile error at the call site) instead of a runtime
98
+ failure. Hydration mechanics — the readiness barrier, stream-id seeding — are
99
+ otherwise unchanged from `mount`.
100
+
101
+ ### `WeftApp.errors`
18
102
 
19
103
  ```ts
20
- mount(node: Renderable, target: Element): Effect<MountHandle, RenderError, R>
104
+ const errors: <R, E>(app: WeftApp<R, E>) => Stream.Stream<UnhandledError>;
21
105
  ```
22
106
 
23
- Renders a Weft `node` into `target` for a fresh (non-SSR) page, building real DOM
24
- and starting every reactive stream. Returns a `MountHandle` whose scope owns the
25
- mounted tree; closing it tears the tree down. Use `mount` for purely client-rendered
26
- apps; use `hydrate` when the markup already exists from SSR.
107
+ The app's unhandled-error stream. While at least one subscriber exists, the default
108
+ `Effect.logError` fallback is suppressed and every `UnhandledError` is delivered to
109
+ all subscribers. With zero subscribers, each unhandled error runs the default log
110
+ (annotated with `weft.region`) instead. No replay a subscriber sees only errors
111
+ published after it subscribed; multiple concurrent subscribers each receive every
112
+ subsequent error. When the last subscriber unsubscribes, the default log resumes.
27
113
 
28
- ### `hydrate`
114
+ ### `WeftApp.dispose`
29
115
 
30
116
  ```ts
31
- hydrate(node: Renderable, target: Element): Effect<MountHandle, HydrationMismatchError | RenderError, R>
117
+ const dispose: <R, E>(app: WeftApp<R, E>) => Effect.Effect<void>;
32
118
  ```
33
119
 
34
- Adopts server-rendered DOM **in place** inside `target` and resumes reactivity
35
- without re-creating elements. The `node` must produce a tree structurally identical
36
- to what the server rendered; a divergence fails with `HydrationMismatchError`. This
37
- is the flash-free path: no second render, the existing nodes simply become live.
120
+ Disposes the app: closes every root scope (in mount order), then releases the
121
+ runtime's layers (`runtime.disposeEffect`), then shuts the error hub down.
122
+ Idempotent teardown effects run once. Subsequent `mount` / `hydrate` calls fail.
38
123
 
39
- ### `mountScoped`
124
+ ### `WeftApp<R, E>` (`WeftAppType`)
40
125
 
41
126
  ```ts
42
- mountScoped(app: Renderable, root: HTMLElement): Effect<MountHandle, UnsupportedNodeTypeError | StreamSubscriptionError | RenderError, Scope.Scope>
127
+ interface WeftApp<in R = never, out E = never> {
128
+ readonly [TypeId]: typeof TypeId;
129
+ readonly runtime: ManagedRuntime.ManagedRuntime<R, E>;
130
+ }
43
131
  ```
44
132
 
45
- Scope-aware `mount`: identical behavior, but requires an ambient `Scope.Scope` in
46
- `R` and registers `unmount` as a finalizer on it, so the mount lives until that
47
- scope closes rather than only until the mount effect resolves. Provide any scoped
48
- layer **outside** a long-lived scoped region so it outlives initial render — see
49
- [Provide Services](https://weftui.dev/docs/how-to/provide-services) for the composition and
50
- [Layer lifetime at the mount](https://weftui.dev/docs/explanation/services-and-context#layer-lifetime-at-the-mount)
51
- for why.
133
+ Re-exported from the barrel as `WeftAppType`. `runtime` is the app's
134
+ `ManagedRuntime`, for running app-level effects against the shared layer outside any
135
+ root e.g. `app.runtime.runFork(trackPageviews)` (see
136
+ `website/src/entry-client.ts`) or `app.runtime.runPromise(Router.push("/about"))`.
52
137
 
53
- ### `hydrateScoped`
138
+ ### `RootHandle`
54
139
 
55
140
  ```ts
56
- hydrateScoped(app: Renderable, root: HTMLElement): Effect<MountHandle, UnsupportedNodeTypeError | StreamSubscriptionError | RenderError | HydrationMismatchError, Scope.Scope>
141
+ interface RootHandle {
142
+ readonly element: HTMLElement;
143
+ unmount(): Effect.Effect<void>;
144
+ }
57
145
  ```
58
146
 
59
- Scope-aware `hydrate` same relationship as `mountScoped` to `mount`, with
60
- `hydrate`'s error union (`HydrationMismatchError` added) and the same client-only
61
- compile-time guard: a server-only requirement left in `app`'s `R` degrades the
62
- return type to `ServerOnlyLeak` via `AssertNoServerOnly`.
147
+ Returned by `mount` / `hydrate`. `element` is the DOM element the root was mounted
148
+ into. `unmount()` closes **this root's scope only**: it interrupts its stream
149
+ subscriptions and any scoped work forked from its event handlers. It does **not**
150
+ dispose the app runtime, touch other roots, or remove the rendered DOM nodes from
151
+ `element`. Idempotent — teardown side effects fire once.
63
152
 
64
- ### `MountHandle`
153
+ ### `UnhandledError`
65
154
 
66
- The handle returned by `mount`, `hydrate`, `mountScoped`, and `hydrateScoped`. Its
67
- `unmount()` interrupts every subscription and event handler and disposes the
68
- mount's `ManagedRuntime`; it does **not** remove the mounted DOM nodes from `root`.
69
- `unmount` is idempotent — safe to call more than once, including once
70
- automatically and once explicitly.
155
+ ```ts
156
+ interface UnhandledError {
157
+ readonly cause: Cause.Cause<unknown>;
158
+ readonly region: string;
159
+ readonly root: RootHandle;
160
+ }
161
+ ```
162
+
163
+ An error that escaped every user-level handler and reached the app's
164
+ unhandled-error hub, published on `WeftApp.errors(app)`. `region` identifies where
165
+ in the render tree the error escaped. Sources (one entry per failing occurrence):
166
+
167
+ - a rendered stream subscription failing or dying with **no enclosing `Boundary`**
168
+ (region e.g. `"attribute:class"`, `"child:stream-3"`),
169
+ - an error escaping the **outermost** `Boundary` recovery (region
170
+ `"boundary:outermost"`),
171
+ - an event-handler effect **failing or dying** (region `"event:onClick"`) — reported
172
+ in development and production alike; there is no `NODE_ENV`-gated swallow.
173
+
174
+ Interrupt-only causes are never published. Errors handled by a nested `Boundary`
175
+ never reach the hub.
176
+
177
+ ### `MountError`
178
+
179
+ ```ts
180
+ type MountError = UnsupportedNodeTypeError | StreamSubscriptionError | RenderError;
181
+ ```
182
+
183
+ Errors `mount` can fail with, beyond the app layer's own error channel `E`.
184
+
185
+ ### `HydrateError`
186
+
187
+ ```ts
188
+ type HydrateError = MountError | HydrationMismatchError;
189
+ ```
190
+
191
+ Everything `MountError` covers, plus `HydrationMismatchError` when the server DOM
192
+ and the node tree diverge.
193
+
194
+ ### `TypeId`
195
+
196
+ ```ts
197
+ const TypeId: unique symbol; // Symbol.for("@weftui/dom/WeftApp")
198
+ ```
71
199
 
72
- The runtime backing the handle lives until `unmount` runs, not until the
73
- `mount`/`hydrate` effect resolves — that effect completes right after initial
74
- render, while streams and handlers keep running in the background. If `mount` or
75
- `hydrate` runs inside a region that supplies an ambient `Scope.Scope` (e.g. under
76
- `Effect.scoped`), `unmount` is auto-registered on that scope as a finalizer, so the
77
- mount tears down when the scope closes; with no ambient scope, behavior is
78
- unchanged and `unmount` must be called explicitly. `mountScoped`/`hydrateScoped`
79
- register the same finalizer explicitly, so the typed variant does not silently
80
- depend on this auto-registration.
200
+ The unique brand for `WeftApp` values. Internal identity marker; rarely referenced
201
+ directly.
81
202
 
82
203
  ## `@weftui/dom/server`
83
204
 
@@ -136,7 +257,7 @@ Re-exports the renderer error types:
136
257
  ## See also
137
258
 
138
259
  - [Render on the Server](https://weftui.dev/docs/how-to/render-on-the-server) — a narrative walkthrough of the server/client split
139
- - [Provide Services](https://weftui.dev/docs/how-to/provide-services) — recipes for value layers, `mountScoped`, and `ManagedRuntime`
260
+ - [Provide Services](https://weftui.dev/docs/how-to/provide-services) — recipes for app layers, scoped layers, and binding an app's lifetime to an external scope
140
261
  - [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model) — hydrate-in-place and why there is no virtual DOM
141
- - [Services and Context](https://weftui.dev/docs/explanation/services-and-context#layer-lifetime-at-the-mount) — why scoped layers need the mount to outlive initial render
262
+ - [Services and Context](https://weftui.dev/docs/explanation/services-and-context) — how services flow from the app layer to every root
142
263
  - [`@weftui/core` reference](https://weftui.dev/docs/reference/core) · [`@weftui/router` reference](https://weftui.dev/docs/reference/router)
@@ -203,7 +203,7 @@ RouterApp<E, R>(def: RouterDef<E, R>): Node<Exclude<E, RouterNotFound>, R | Rout
203
203
 
204
204
  The universal router root node — render this on both server and client. Wraps the nested outlet in the router's internal not-found boundary, so a `RouterNotFound` raised by a page renders the configured `notFound` page in place. Server dispatch runs through `HttpApiBuilder`: a page-raised `RouterNotFound` and a no-match surface their 404 through the platform request pipeline.
205
205
 
206
- `RouterApp` requires `Router` in its environment — provide it via `RouterLive` (client) or `RouterServer` (server), not `Effect.provide` at the node level (that would release the scoped layer immediately).
206
+ `RouterApp` requires `Router` in its environment — provide it via `RouterLive` (client) or `RouterServer` (server), not `Effect.provide` at the node level services under `WeftApp` come exclusively from the app's layer, not ambient `Effect.provide`.
207
207
 
208
208
  ### `outletNode` (a.k.a. `RouterOutlet`)
209
209
 
@@ -224,11 +224,11 @@ RouterLive(
224
224
  ): Layer.Layer<Router | AppRpcClientTag>;
225
225
  ```
226
226
 
227
- The client `Router` layer, backed by the History API. Seeds a `SubscriptionRef` from `window.location`, listens for `popstate`, installs the same-origin link-click interceptor, and derives the `HttpApiClient` exposed as `Router.httpApiClient` (over `FetchHttpClient`; `baseUrl` defaults to same-origin). Alongside `Router` it also provides the core [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam — a **network** flat rpc client over the app's merged `RpcGroup` (`RpcClient.make` → `POST /_eui/rpc`) — so `@weftui/dom` can resolve a [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) (hydrated refetch and client-first SPA mount) without depending on this package or `effect/unstable/rpc`. Pass the same merged `group` the server wires into [`RouterServer`](#routerserver). **Scoped** — it must outlive the mount, so provide it through a `ManagedRuntime`:
227
+ The client `Router` layer, backed by the History API. Seeds a `SubscriptionRef` from `window.location`, listens for `popstate`, installs the same-origin link-click interceptor, and derives the `HttpApiClient` exposed as `Router.httpApiClient` (over `FetchHttpClient`; `baseUrl` defaults to same-origin). Alongside `Router` it also provides the core [`AppRpcClientTag`](https://weftui.dev/docs/reference/core#apprpcclienttag) seam — a **network** flat rpc client over the app's merged `RpcGroup` (`RpcClient.make` → `POST /_eui/rpc`) — so `@weftui/dom` can resolve a [`Boundary.rpc`](https://weftui.dev/docs/reference/core#boundaryrpc) (hydrated refetch and client-first SPA mount) without depending on this package or `effect/unstable/rpc`. Pass the same merged `group` the server wires into [`RouterServer`](#routerserver). **Scoped** — it must outlive the mount; give it to `WeftApp.make` and the app runtime owns its lifetime (built lazily on first mount, released at `WeftApp.dispose`):
228
228
 
229
229
  ```typescript
230
- const runtime = ManagedRuntime.make(RouterLive(App, { rpc: { group: StockRpcs } }));
231
- void runtime.runPromise(hydrate(RouterApp(App), root));
230
+ const app = WeftApp.make(RouterLive(App, { rpc: { group: StockRpcs } }));
231
+ void Effect.runPromise(WeftApp.hydrate(app, RouterApp(App), root));
232
232
  ```
233
233
 
234
234
  ### Programmatic navigation
@@ -323,7 +323,7 @@ These power the runtime and are exported for tooling/tests; most apps never touc
323
323
  | Export | Description |
324
324
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
325
325
  | `compile(def)` | Walks a tree into flat `CompiledLeaf`s with merged path/query schemas and layout chains. |
326
- | `buildHttpApi(leaves)` | Builds the authoritative `HttpApi` (one `"pages"` group, a GET endpoint per leaf with `setPath`/`setUrlParams` + 404). Called by `Router.router`; the result is `def.httpApi`. |
326
+ | `buildHttpApi(leaves)` | Builds the authoritative `HttpApi` (one `"pages"` group, a GET endpoint per leaf with `params`/`query` schemas + 404). Called by `Router.router`; the result is `def.httpApi`. |
327
327
  | `leafRegistry` | `WeakMap<RouteNode, CompiledLeaf>` read by `href` to resolve a leaf's pattern/schemas. |
328
328
  | `match(compiled, url)` | Resolves a URL to a `RouteMatch` (`Matched` with decoded `path`/`query`, or `NotFound`). |
329
329
  | `compileMatchers(compiled)` | Precompiles per-leaf regex matchers. |
@@ -14,9 +14,11 @@ We assume you know [Effect](https://effect.website/docs/getting-started/introduc
14
14
  ## Install
15
15
 
16
16
  ```bash
17
- npm install @weftui/core @weftui/dom effect
17
+ npm install @weftui/core @weftui/dom effect@beta
18
18
  ```
19
19
 
20
+ 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.
21
+
20
22
  `@weftui/core` gives you the element builders and combinators; `@weftui/dom` renders them (its `./client` entry mounts in the browser). `effect` is the peer everything is built on.
21
23
 
22
24
  ## Build a component
@@ -25,14 +27,15 @@ A **component is a plain function you call** — there is no JSX and no `<Compon
25
27
 
26
28
  ```typescript
27
29
  import { h } from "@weftui/core";
28
- import { mount } from "@weftui/dom/client";
30
+ import { WeftApp } from "@weftui/dom/client";
29
31
  import { Effect } from "effect";
30
32
 
31
33
  function App() {
32
34
  return h.div({ class: "app" }, [h.h1("Hello, Weft"), h.p("A minimal app.")]);
33
35
  }
34
36
 
35
- void Effect.runPromise(mount(App(), document.getElementById("root")!));
37
+ const app = WeftApp.make();
38
+ void Effect.runPromise(WeftApp.mount(app, App(), document.getElementById("root")!));
36
39
  ```
37
40
 
38
41
  The `h` namespace is the entry point: every property (`h.div`, `h.h1`, `h.button`, …) is a builder for that HTML tag. A builder takes optional props and children, and returns a `Node`.
@@ -40,7 +43,7 @@ The `h` namespace is the entry point: every property (`h.div`, `h.h1`, `h.button
40
43
  ## What just happened
41
44
 
42
45
  - `App()` returns a **`Node<never, never>`** — the two type parameters are the error channel (`E`) and the requirement channel (`R`), both `never` here because this component neither fails nor needs a service. As your app grows, those channels accumulate what it can fail with and what it depends on. That is the whole point of Weft's types — see [The Rendering Model](https://weftui.dev/docs/explanation/rendering-model).
43
- - `mount(node, target)` renders the node into `target`, building real DOM and starting any reactive streams. It returns an `Effect<MountHandle>`; run it with your Effect runtime (`Effect.runPromise` is fine for a script).
46
+ - `WeftApp.make()` creates a Weft app — synchronously, with no layer to build yet. `WeftApp.mount(app, node, target)` renders the node into `target`, building real DOM and starting any reactive streams. It returns `Effect<RootHandle, …>` with `R = never`, so a bare `Effect.runPromise` runs it no `Effect.provide` needed. You will give `WeftApp.make` a `Layer` once components need services — see [Services and Async](https://weftui.dev/docs/tutorial/03-services-and-async).
44
47
  - The component function runs **once**. Nothing here re-runs on a timer or a state change — because there is no state yet. That comes next.
45
48
 
46
49
  ## Next
@@ -15,7 +15,7 @@ Use Effect's `SubscriptionRef` for component-local state. `SubscriptionRef.chang
15
15
 
16
16
  ```typescript
17
17
  import { h } from "@weftui/core";
18
- import { mount } from "@weftui/dom/client";
18
+ import { WeftApp } from "@weftui/dom/client";
19
19
  import { Effect, SubscriptionRef } from "effect";
20
20
 
21
21
  const Counter = () =>
@@ -29,7 +29,8 @@ const Counter = () =>
29
29
  ]);
30
30
  });
31
31
 
32
- void Effect.runPromise(mount(Counter(), document.getElementById("root")!));
32
+ const app = WeftApp.make();
33
+ void Effect.runPromise(WeftApp.mount(app, Counter(), document.getElementById("root")!));
33
34
  ```
34
35
 
35
36
  `Effect.gen` lets you `yield*` the `SubscriptionRef` to set up state **before** building the tree. Because a `Node` is an `Effect`, the component body is an ordinary generator — no hooks, no dependency arrays.
@@ -11,12 +11,12 @@ description: Give handlers access to services from the environment, and render a
11
11
 
12
12
  ## Handlers that use services
13
13
 
14
- An event handler can **return an Effect**, and that Effect runs in the component's environment — so it can read any service you provide at the mount boundary:
14
+ An event handler can **return an Effect**, and that Effect runs in the app's environment — so it can read any service the app's layer provides:
15
15
 
16
16
  ```typescript
17
17
  import { h } from "@weftui/core";
18
- import { mount } from "@weftui/dom/client";
19
- import { Context, Effect, Layer, pipe } from "effect";
18
+ import { WeftApp } from "@weftui/dom/client";
19
+ import { Context, Effect, Layer } from "effect";
20
20
 
21
21
  class Logger extends Context.Service<Logger, { log: (message: string) => Effect.Effect<void> }>()(
22
22
  "Logger",
@@ -38,13 +38,12 @@ const LogButton = () =>
38
38
  "Log",
39
39
  );
40
40
 
41
- // Provide the layer at mount — every handler in the tree can now read Logger.
42
- void Effect.runPromise(
43
- pipe(mount(LogButton(), document.getElementById("root")!), Effect.provide(LoggerLive)),
44
- );
41
+ // Give the app the layer — every handler in every root can now read Logger.
42
+ const app = WeftApp.make(LoggerLive);
43
+ void Effect.runPromise(WeftApp.mount(app, LogButton(), document.getElementById("root")!));
45
44
  ```
46
45
 
47
- `Logger` entered the tree's requirement channel the moment `LogButton` read it, and you discharged it **once**, at `mount`, with `Effect.provide`. Provide too little and it is a compile error at the mount call. This is Weft's entire dependency-injection story — it is just Effect's. The deeper treatment is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
46
+ `Logger` entered the tree's requirement channel the moment `LogButton` read it, and you discharged it **once**, by passing `LoggerLive` to `WeftApp.make`. Provide too little and it is a compile error the type of `app` (and so of `WeftApp.mount(app, LogButton(), …)`) names exactly which service is missing. Services come exclusively from the app's layer: an `Effect.provide` wrapped around the `mount` call does **not** reach components or handlers. This is Weft's entire dependency-injection story — it is just Effect's. The deeper treatment is [Services and Context](https://weftui.dev/docs/explanation/services-and-context).
48
47
 
49
48
  ## Async loading states
50
49
 
@@ -52,7 +51,7 @@ A component can return a **`Stream<Node>`** to show different content over time.
52
51
 
53
52
  ```typescript
54
53
  import { h } from "@weftui/core";
55
- import { mount } from "@weftui/dom/client";
54
+ import { WeftApp } from "@weftui/dom/client";
56
55
  import { Effect, Stream } from "effect";
57
56
 
58
57
  const AsyncGreeting = ({ name }: { name: string }) =>
@@ -66,7 +65,10 @@ const AsyncGreeting = ({ name }: { name: string }) =>
66
65
  ),
67
66
  );
68
67
 
69
- void Effect.runPromise(mount(AsyncGreeting({ name: "World" }), document.getElementById("root")!));
68
+ const app = WeftApp.make();
69
+ void Effect.runPromise(
70
+ WeftApp.mount(app, AsyncGreeting({ name: "World" }), document.getElementById("root")!),
71
+ );
70
72
  ```
71
73
 
72
74
  The stream emits the loading node first, then the resolved node — the renderer swaps the DOM in place on the second emission. This is the raw mechanism; for coordinating _several_ async regions with a single fallback, reach for [`Boundary.suspend`](https://weftui.dev/docs/explanation/boundaries-and-suspense), which you will meet in the next step.
@@ -42,11 +42,12 @@ export const render = () => Effect.runPromise(renderToStringHydratable(App()));
42
42
 
43
43
  ```typescript
44
44
  // client entry
45
- import { hydrate } from "@weftui/dom/client";
45
+ import { WeftApp } from "@weftui/dom/client";
46
46
  import { Effect } from "effect";
47
47
  import { App } from "./app";
48
48
 
49
- void Effect.runPromise(hydrate(App(), document.getElementById("root")!));
49
+ const app = WeftApp.make();
50
+ void Effect.runPromise(WeftApp.hydrate(app, App(), document.getElementById("root")!));
50
51
  ```
51
52
 
52
53
  The same side-effect-free `App` is imported by both entries. For server-resolved data that replays into the client without a second request, `Boundary.rpc` extends this model — resolve an rpc on the server, serialize its result into the HTML, replay it on hydrate, then keep the region live for refetch.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weftui/dom",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
4
4
  "description": "DOM renderer for Weft — mount and hydrate in the browser, render to string or stream on the server",
5
5
  "keywords": [
6
6
  "dom",
@@ -44,19 +44,20 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@weftui/core": "0.27.0"
47
+ "@weftui/core": "0.28.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/jsdom": "^28.0.3",
51
- "@types/node": "^25.9.2",
52
- "effect": "4.0.0-beta.93",
51
+ "@types/node": "^26.1.1",
52
+ "effect": "4.0.0-beta.98",
53
53
  "jsdom": "^29.1.1",
54
- "tsx": "^4.22.4",
55
- "typescript": "^6.0.3",
56
- "vite": "npm:@voidzero-dev/vite-plus-core@0.2.2",
57
- "vite-plus": "0.2.2"
54
+ "tstyche": "^7.2.2",
55
+ "tsx": "^4.23.1",
56
+ "typescript": "^7.0.2",
57
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.5",
58
+ "vite-plus": "0.2.5"
58
59
  },
59
60
  "peerDependencies": {
60
- "effect": "4.0.0-beta.93"
61
+ "effect": ">=4.0.0-beta.98 <4.0.0"
61
62
  }
62
63
  }