@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.
- package/README.md +14 -9
- package/dist/client/index.d.ts +159 -146
- package/dist/client/index.js +1 -1
- package/dist/{data-Bk7mnjdd.d.ts → data-C88W1AwU.d.ts} +4 -5
- package/dist/index.d.ts +1 -1
- package/dist/rolldown-runtime-DK3Fl9T5.js +1 -0
- package/dist/server/index.d.ts +0 -1
- package/docs/explanation/boundaries-and-suspense.md +2 -2
- package/docs/explanation/reactive-primitives.md +1 -1
- package/docs/explanation/rendering-model.md +2 -2
- package/docs/explanation/services-and-context.md +13 -17
- package/docs/how-to/add-routing.md +7 -7
- package/docs/how-to/author-components.md +7 -5
- package/docs/how-to/load-data-with-rpc.md +4 -2
- package/docs/how-to/provide-services.md +52 -74
- package/docs/how-to/render-on-the-server.md +4 -3
- package/docs/index.md +2 -2
- package/docs/reference/dom.md +166 -45
- package/docs/reference/router.md +5 -5
- package/docs/tutorial/01-your-first-app.md +7 -4
- package/docs/tutorial/02-reactivity.md +3 -2
- package/docs/tutorial/03-services-and-async.md +12 -10
- package/docs/tutorial/04-errors-and-server.md +3 -2
- package/package.json +10 -9
package/docs/reference/dom.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
+
const errors: <R, E>(app: WeftApp<R, E>) => Stream.Stream<UnhandledError>;
|
|
21
105
|
```
|
|
22
106
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
### `
|
|
114
|
+
### `WeftApp.dispose`
|
|
29
115
|
|
|
30
116
|
```ts
|
|
31
|
-
|
|
117
|
+
const dispose: <R, E>(app: WeftApp<R, E>) => Effect.Effect<void>;
|
|
32
118
|
```
|
|
33
119
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
### `
|
|
124
|
+
### `WeftApp<R, E>` (`WeftAppType`)
|
|
40
125
|
|
|
41
126
|
```ts
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
### `
|
|
138
|
+
### `RootHandle`
|
|
54
139
|
|
|
55
140
|
```ts
|
|
56
|
-
|
|
141
|
+
interface RootHandle {
|
|
142
|
+
readonly element: HTMLElement;
|
|
143
|
+
unmount(): Effect.Effect<void>;
|
|
144
|
+
}
|
|
57
145
|
```
|
|
58
146
|
|
|
59
|
-
|
|
60
|
-
`
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
### `
|
|
153
|
+
### `UnhandledError`
|
|
65
154
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
73
|
-
|
|
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
|
|
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
|
|
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)
|
package/docs/reference/router.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
231
|
-
void
|
|
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 `
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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 {
|
|
19
|
-
import { Context, Effect, Layer
|
|
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
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
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**,
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
45
|
+
import { WeftApp } from "@weftui/dom/client";
|
|
46
46
|
import { Effect } from "effect";
|
|
47
47
|
import { App } from "./app";
|
|
48
48
|
|
|
49
|
-
|
|
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.
|
|
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.
|
|
47
|
+
"@weftui/core": "0.28.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/jsdom": "^28.0.3",
|
|
51
|
-
"@types/node": "^
|
|
52
|
-
"effect": "4.0.0-beta.
|
|
51
|
+
"@types/node": "^26.1.1",
|
|
52
|
+
"effect": "4.0.0-beta.98",
|
|
53
53
|
"jsdom": "^29.1.1",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"vite
|
|
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.
|
|
61
|
+
"effect": ">=4.0.0-beta.98 <4.0.0"
|
|
61
62
|
}
|
|
62
63
|
}
|