@farm.js/islx 0.0.2 → 0.0.4
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/CHANGELOG.md +28 -0
- package/README.md +2 -1
- package/SPEC.md +15 -4
- package/dist/_chunks/{actions-2sTj0A2T.mjs → actions-8RQfrM-p.mjs} +2 -2
- package/dist/_chunks/{actions-B8dJ8f7L.mjs → actions-CtLpJPEB.mjs} +2 -2
- package/dist/_chunks/{codec-DqfVYKsx.mjs → codec-C8to-RQW.mjs} +3 -2
- package/dist/_chunks/{schedule-BFoGKRrU.mjs → schedule-CjoybS3b.mjs} +1 -1
- package/dist/_chunks/{tree-fID6sz25.d.mts → tree-B4RMU2UX.d.mts} +38 -1
- package/dist/_chunks/{tree-BDB5VEao.mjs → tree-opIpaCke.mjs} +226 -55
- package/dist/_chunks/{wire-C3Rpbwl6.d.mts → wire-Dipqh-EB.d.mts} +11 -1
- package/dist/_chunks/{wire-CXOodE06.mjs → wire-QEQWBr5s.mjs} +11 -1
- package/dist/client.mjs +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/react/client.d.mts +9 -3
- package/dist/react/client.mjs +147 -14
- package/dist/react/server.d.mts +2 -2
- package/dist/react/server.mjs +24 -6
- package/dist/react.d.mts +2 -2
- package/dist/react.mjs +1 -1
- package/dist/server.mjs +3 -3
- package/dist/vite.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.4
|
|
4
|
+
|
|
5
|
+
Promise props, and islands in a page that streams after it loaded.
|
|
6
|
+
|
|
7
|
+
- A server component can pass an unawaited promise to a client component, which reads it with
|
|
8
|
+
`use()`. The promise travels as `{"$":"T","v":name}`; when it settles, a Suspense boundary after
|
|
9
|
+
the island writes `<script type="f/promise" name>` with `{"v":value}` (the same codec and values
|
|
10
|
+
sent once) or `{"e":1}`. A rejection's reason stays on the server. In the browser the prop is a
|
|
11
|
+
thenable: settled already when its outcome is on the page, so hydration does not suspend, and
|
|
12
|
+
settled when it streams in otherwise.
|
|
13
|
+
- An island holding a Suspense boundary that is still streaming, after the document has loaded (a
|
|
14
|
+
framework router that streams navigations), hydrates once React reveals the boundary instead of
|
|
15
|
+
React rendering it again in the browser (error #419). `endStreamedContent(root)` from
|
|
16
|
+
`@farm.js/islx/react/client` marks boundaries a stream never completed as rendered in the
|
|
17
|
+
browser, so their islands hydrate on the next pass.
|
|
18
|
+
|
|
19
|
+
## 0.0.3
|
|
20
|
+
|
|
21
|
+
Server layouts as trees.
|
|
22
|
+
|
|
23
|
+
- A server layout around a client provider renders as a server tree, so a framework no longer
|
|
24
|
+
ships the layout's module to hydrate it. On the server, `renderServerTree(layout, { outlets,
|
|
25
|
+
scope })` with `<Outlet />` where the page goes; in the browser, `createServerTreeLayout(source,
|
|
26
|
+
scope, { load, ServerHtml, onUpdate })` and `layout.render({ children: page })`. Rendering it
|
|
27
|
+
again with another page keeps the provider's state.
|
|
28
|
+
- A layout's tree names its payload, chunks and server HTML by its scope (`f/chunk name="/:0"`),
|
|
29
|
+
so it and the page's tree at its outlet never read each other's parts. New tag `L` (outlet).
|
|
30
|
+
|
|
3
31
|
## 0.0.2
|
|
4
32
|
|
|
5
33
|
Streaming and hydration fixes.
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ islx started as [Farm.js](https://github.com/farming-labs/farm.js)'s islands mod
|
|
|
13
13
|
replaces React Server Components' Flight payload. On a 300-page docs app it ships 26% less
|
|
14
14
|
JavaScript, and a client navigation sends 2.5 KB instead of 12 KB.
|
|
15
15
|
|
|
16
|
-
> Status: 0.0.
|
|
16
|
+
> Status: 0.0.4. React and Vite are supported; other renderers and framework
|
|
17
17
|
> integrations follow. The wire format is in [SPEC.md](./SPEC.md), changes in
|
|
18
18
|
> [CHANGELOG.md](./CHANGELOG.md).
|
|
19
19
|
|
|
@@ -180,6 +180,7 @@ A framework that renders pages itself (Farm does) uses the pieces directly inste
|
|
|
180
180
|
| Render islands in its own render | wrap the page in `<IslandsProvider held={parseHeldValues(request)}>` |
|
|
181
181
|
| A layout that hydrates as one root | `withinClientRoot(layout)`; its server-HTML page: `isolateIslands(page)` |
|
|
182
182
|
| An async page under such a layout | `renderServerTree(page, { held, payloadOnly })` on the server; `createServerTreeElement(source, { load, ServerHtml })` in the browser |
|
|
183
|
+
| A server layout around a client provider, without shipping the layout | `renderServerTree(<Layout><Outlet /></Layout>, { outlets: { children: page }, scope })` on the server; `createServerTreeLayout(source, scope, { load, ServerHtml, onUpdate })` in the browser, then `layout.render({ children: page })` |
|
|
183
184
|
| Hydrate | `createHydrationRuntime({ load, schedule?, wrap?, report? })`, `createServerHtml(runtime)` |
|
|
184
185
|
| Hold early clicks | inline `PREHYDRATION_SCRIPT` in the page |
|
|
185
186
|
| Actions | `handleAction(request, { allowedOrigins, bodySizeLimit, run, mapError, renderFormState })`; `setActionNavigator({ refresh, navigate })` |
|
package/SPEC.md
CHANGED
|
@@ -32,11 +32,12 @@ Anything that writes and reads it, in any language or framework, is a islx imple
|
|
|
32
32
|
| `<script type="f/props" for>` | An island's props, as JSON. `for` names the island's `props` id. |
|
|
33
33
|
| `<script type="f/value" name>` | A value sent once. `name` is its key. Written before the first island that uses it. |
|
|
34
34
|
| `<f-slot name>` | Server output passed to an island as children. It stays server HTML; the island renders it in place. |
|
|
35
|
-
| `<script type="f/tree" [partial]>` | A
|
|
36
|
-
| `<script type="f/chunk" name>` | A part of a server tree that streamed in after the tree (a resolved Suspense boundary). |
|
|
37
|
-
| `<f-html name>` | Server HTML inside a server tree. |
|
|
35
|
+
| `<script type="f/tree" [name] [partial]>` | A server tree, so the browser can rebuild the page inside an existing root without its HTML. A page's tree has no `name`; a layout's carries its scope and sits at its `children` outlet, or after the layout when it has none. `partial` marks a navigation that sent only the tree. |
|
|
36
|
+
| `<script type="f/chunk" name>` | A part of a server tree that streamed in after the tree (a resolved Suspense boundary). `name` is its number, after `scope:` in a layout's tree. |
|
|
37
|
+
| `<f-html name>` | Server HTML inside a server tree, named like a chunk. |
|
|
38
|
+
| `<script type="f/promise" name>` | The outcome of a promise an island received, written when it settles: `{"v":value}`, or `{"e":1}` for a rejection (the reason stays on the server). `name` is the promise's name. It may land anywhere after the island, and stream in after the island hydrated. |
|
|
38
39
|
|
|
39
|
-
A value script's and
|
|
40
|
+
A value script's, props script's and promise script's JSON escapes `<`, U+2028 and U+2029.
|
|
40
41
|
|
|
41
42
|
## JSON tags
|
|
42
43
|
|
|
@@ -58,7 +59,10 @@ with any other keys is data.
|
|
|
58
59
|
| `U` | `URL` | `href` |
|
|
59
60
|
| `R` | `RegExp` | `[source, flags]` |
|
|
60
61
|
| `Y` | bytes | `[type, base64]`; type is `ArrayBuffer`, `DataView` or a typed array name |
|
|
62
|
+
| `T` | a promise an island receives | its name; its outcome arrives in the `f/promise` script of that name |
|
|
63
|
+
| `P` | a promise inside a server tree, resolved already | its value, tagged |
|
|
61
64
|
| `p` | a Suspense boundary inside a server tree | |
|
|
65
|
+
| `L` | a layout tree's outlet, where the page renders | `n`: the outlet's name, `children` for the page |
|
|
62
66
|
| `F` | a `FormData` action argument | its index among the call's attachments |
|
|
63
67
|
| `K` | a `Blob` or `File` action argument | its index among the call's attachments |
|
|
64
68
|
|
|
@@ -101,6 +105,13 @@ islands, and handles history, scroll and focus. Anything unusual loads the page
|
|
|
101
105
|
site, `target`, `download`, `rel="external"`, modifier keys, a non-HTML answer, or a page whose
|
|
102
106
|
browser entry differs, as after a deploy).
|
|
103
107
|
|
|
108
|
+
A framework router may stream a navigation in: the shell first, then each Suspense boundary as
|
|
109
|
+
it completes. A renderer may hydrate a boundary that is still streaming only while the document
|
|
110
|
+
loads, so after that an island holding one waits, and the next hydration pass starts it once the
|
|
111
|
+
boundary is revealed. When the stream ends, boundaries it never completed are marked as rendered
|
|
112
|
+
in the browser, and the next pass hydrates their islands (React: a pending boundary is
|
|
113
|
+
`<!--$?-->`, marked `<!--$!-->` by `endStreamedContent(root)` from `@farm.js/islx/react/client`).
|
|
114
|
+
|
|
104
115
|
A server that leaves layouts out renders each kept layout as an empty boundary, and the browser
|
|
105
116
|
keeps its own. If the response does not fit what is on screen, the browser loads the full
|
|
106
117
|
document. All three headers make a response private, and the server sends `Vary` for each one
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { a as encode, r as decode, u as setOwn } from "./codec-
|
|
1
|
+
import { A as VALUES_HEADER, F as getTag, M as VALUE_SCRIPT_TYPE, N as attachmentField, j as VALUE_KEY_PATTERN, o as BOUND_FIELD, s as CALL_FIELD, t as ACTION_HEADER } from "./wire-QEQWBr5s.mjs";
|
|
2
|
+
import { a as encode, r as decode, u as setOwn } from "./codec-C8to-RQW.mjs";
|
|
3
3
|
// src/client/value-store.ts
|
|
4
4
|
/**
|
|
5
5
|
* The browser's store of values sent once. It keeps every value it has read, parses each once,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as encode, c as needsObjectTag, o as encodeLeaf, r as decode, t as EncodeError } from "./codec-
|
|
1
|
+
import { A as VALUES_HEADER, P as formFieldName, d as FORM_REF_PREFIX, j as VALUE_KEY_PATTERN } from "./wire-QEQWBr5s.mjs";
|
|
2
|
+
import { a as encode, c as needsObjectTag, o as encodeLeaf, r as decode, t as EncodeError } from "./codec-C8to-RQW.mjs";
|
|
3
3
|
import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
|
|
4
4
|
// src/server/values.ts
|
|
5
5
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { F as getTag } from "./wire-QEQWBr5s.mjs";
|
|
2
2
|
// src/protocol/codec.ts
|
|
3
3
|
/**
|
|
4
4
|
* Values JSON cannot hold. Each travels as a tagged object: the reserved key `$` holds a short tag
|
|
@@ -117,7 +117,8 @@ function decodeTagged(object, decodeChild) {
|
|
|
117
117
|
case "s":
|
|
118
118
|
case "a":
|
|
119
119
|
case "F":
|
|
120
|
-
case "K":
|
|
120
|
+
case "K":
|
|
121
|
+
case "T": return NOT_A_VALUE;
|
|
121
122
|
case "o":
|
|
122
123
|
if (data && typeof data === "object" && !Array.isArray(data)) return plainObject(Object.entries(data).map(([key, value]) => [key, decodeChild(value)]));
|
|
123
124
|
break;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as hasPendingInteraction, o as takePendingInteractions, t as INTERACTION_EVENT } from "./prehydration-B1GLTLdk.mjs";
|
|
2
|
-
import { m as HYDRATED_ATTRIBUTE } from "./wire-
|
|
2
|
+
import { m as HYDRATED_ATTRIBUTE } from "./wire-QEQWBr5s.mjs";
|
|
3
3
|
// src/client/schedule.ts
|
|
4
4
|
/**
|
|
5
5
|
* When an island hydrates. Renderer-neutral: every adapter schedules through this.
|
|
@@ -58,6 +58,13 @@ declare function createIsland<P extends object>(Component: React.ComponentType<P
|
|
|
58
58
|
* page in its layout's React root and never shows its HTML, so the server leaves the HTML out.
|
|
59
59
|
*/
|
|
60
60
|
declare const PAYLOAD_ONLY_ATTRIBUTE = "partial";
|
|
61
|
+
/**
|
|
62
|
+
* Where a layout rendered as a server tree places the page (or another outlet, by `name`). The
|
|
63
|
+
* tree records the outlet, and the server and the browser render what they are given there.
|
|
64
|
+
*/
|
|
65
|
+
declare function Outlet(_props: {
|
|
66
|
+
name?: string;
|
|
67
|
+
}): null;
|
|
61
68
|
interface ServerTreeRenderOptions {
|
|
62
69
|
/**
|
|
63
70
|
* Renders a part the resolver cannot represent, as server HTML whose client components are
|
|
@@ -71,6 +78,17 @@ interface ServerTreeRenderOptions {
|
|
|
71
78
|
* streams and the server HTML parts it cannot represent, without the page's rendered HTML.
|
|
72
79
|
*/
|
|
73
80
|
payloadOnly?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* A layout rendered as a server tree: what each `<Outlet>` renders, by name. The payload script
|
|
83
|
+
* then sits at the `children` outlet, inside the layout's markup, so a layout that renders the
|
|
84
|
+
* whole document keeps it in the body.
|
|
85
|
+
*/
|
|
86
|
+
outlets?: Record<string, React.ReactNode>;
|
|
87
|
+
/**
|
|
88
|
+
* Names this tree's payload, chunks and server HTML parts, so a layout's tree and the trees at
|
|
89
|
+
* its outlet do not read each other's. A page's tree has none.
|
|
90
|
+
*/
|
|
91
|
+
scope?: string;
|
|
74
92
|
}
|
|
75
93
|
/**
|
|
76
94
|
* Resolves a server page and returns what renders in its place: the resolved tree and its
|
|
@@ -99,4 +117,23 @@ interface ServerTreeElementOptions {
|
|
|
99
117
|
* navigation), after loading its client modules. Returns null for a page without a tree.
|
|
100
118
|
*/
|
|
101
119
|
declare function createServerTreeElement(source: ParentNode | string, options: ServerTreeElementOptions): Promise<React.ReactElement | null>;
|
|
102
|
-
|
|
120
|
+
/** A layout rebuilt from its server tree. */
|
|
121
|
+
interface ServerTreeLayout {
|
|
122
|
+
/** The layout around these outlets: plain elements, the same tree the server rendered. */
|
|
123
|
+
render(outlets: Record<string, React.ReactNode>): React.ReactNode;
|
|
124
|
+
/** Stops following hot module updates. */
|
|
125
|
+
dispose(): void;
|
|
126
|
+
}
|
|
127
|
+
interface ServerTreeLayoutOptions extends Omit<ServerTreeElementOptions, "key"> {
|
|
128
|
+
/** Runs after a hot update to one of the layout's client modules: render the route again. */
|
|
129
|
+
onUpdate?(): void;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Rebuilds a layout the server rendered with `renderServerTree(..., { outlets, scope })`, from
|
|
133
|
+
* its server HTML or a navigation's HTML. The layout's own module never reaches the browser: its
|
|
134
|
+
* client components load from the tree, and `render(outlets)` places the page at its outlet.
|
|
135
|
+
* Rendering it with the same component types at the same place keeps its client state across
|
|
136
|
+
* navigations. Returns null when `source` has no tree for `scope`.
|
|
137
|
+
*/
|
|
138
|
+
declare function createServerTreeLayout(source: ParentNode | string, scope: string, options: ServerTreeLayoutOptions): Promise<ServerTreeLayout | null>;
|
|
139
|
+
export { setRenderStateResolver as _, ServerTreeLayoutOptions as a, createServerTreeLayout as c, ClientReference as d, IslandRenderState as f, isolateIslands as g, getClientReference as h, ServerTreeLayout as i, renderServerTree as l, createIsland as m, PAYLOAD_ONLY_ATTRIBUTE as n, ServerTreeRenderOptions as o, IslandsProvider as p, ServerTreeElementOptions as r, createServerTreeElement as s, Outlet as t, updateServerTrees as u, withinClientRoot as v };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-
|
|
2
|
-
import {
|
|
3
|
-
import { l as getValueStore, t as createServerReference } from "./actions-
|
|
1
|
+
import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-C8to-RQW.mjs";
|
|
2
|
+
import { M as VALUE_SCRIPT_TYPE, c as CHUNK_SCRIPT_TYPE, h as ISLAND_ELEMENT, k as TREE_SCRIPT_TYPE, p as HTML_ELEMENT, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "./wire-QEQWBr5s.mjs";
|
|
3
|
+
import { l as getValueStore, t as createServerReference } from "./actions-8RQfrM-p.mjs";
|
|
4
4
|
import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
import * as ReactDOM from "react-dom";
|
|
@@ -141,6 +141,64 @@ function splitSlots(value, prefix, counter) {
|
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
const warned = /* @__PURE__ */ new Set();
|
|
144
|
+
const outcomes = /* @__PURE__ */ new WeakMap();
|
|
145
|
+
function settle(promise) {
|
|
146
|
+
let outcome = outcomes.get(promise);
|
|
147
|
+
if (!outcome) {
|
|
148
|
+
outcome = Promise.resolve(promise).then((value) => ({
|
|
149
|
+
ok: true,
|
|
150
|
+
value
|
|
151
|
+
}), (reason) => ({
|
|
152
|
+
ok: false,
|
|
153
|
+
reason
|
|
154
|
+
}));
|
|
155
|
+
outcomes.set(promise, outcome);
|
|
156
|
+
}
|
|
157
|
+
return outcome;
|
|
158
|
+
}
|
|
159
|
+
const REJECTED = "{\"e\":1}";
|
|
160
|
+
/** Each promise an island received, in a boundary of its own: its outcome once it settles. */
|
|
161
|
+
function promiseBoundaries(promises, label) {
|
|
162
|
+
return promises.map(({ name, promise }) => React.createElement(React.Suspense, {
|
|
163
|
+
key: name,
|
|
164
|
+
fallback: null
|
|
165
|
+
}, React.createElement(IslandPromise, {
|
|
166
|
+
name,
|
|
167
|
+
promise,
|
|
168
|
+
label
|
|
169
|
+
})));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A promise's outcome, written when it settles. Its values are sent whether or not the page wrote
|
|
173
|
+
* them already: this script may land before an earlier island's, wherever React puts it.
|
|
174
|
+
*/
|
|
175
|
+
function IslandPromise({ name, promise, label }) {
|
|
176
|
+
const outcome = React.use(settle(promise));
|
|
177
|
+
const state = React.useContext(getContexts().render) ?? resolveRenderState();
|
|
178
|
+
const hook = getIslandServerHook();
|
|
179
|
+
let encoded = {
|
|
180
|
+
json: REJECTED,
|
|
181
|
+
values: [],
|
|
182
|
+
promises: []
|
|
183
|
+
};
|
|
184
|
+
if (outcome.ok) try {
|
|
185
|
+
encoded = hook.encode({ v: outcome.value }, state?.held ?? null);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.warn(`[islx] A promise passed to ${label} resolved to a value that cannot be sent to the browser: it rejects there.`, error);
|
|
188
|
+
}
|
|
189
|
+
else console.error(`[islx] A promise passed to ${label} rejected.`, outcome.reason);
|
|
190
|
+
return React.createElement(React.Fragment, null, ...encoded.values.map((value) => React.createElement("script", {
|
|
191
|
+
key: value.key,
|
|
192
|
+
type: VALUE_SCRIPT_TYPE,
|
|
193
|
+
name: value.key,
|
|
194
|
+
dangerouslySetInnerHTML: { __html: hook.escape(value.json) }
|
|
195
|
+
})), React.createElement("script", {
|
|
196
|
+
key: "outcome",
|
|
197
|
+
type: PROMISE_SCRIPT_TYPE,
|
|
198
|
+
name,
|
|
199
|
+
dangerouslySetInnerHTML: { __html: hook.escape(encoded.json) }
|
|
200
|
+
}), ...promiseBoundaries(encoded.promises, label));
|
|
201
|
+
}
|
|
144
202
|
function isThenable$1(value) {
|
|
145
203
|
return !!value && typeof value.then === "function";
|
|
146
204
|
}
|
|
@@ -193,7 +251,7 @@ function createIsland(Component, src, exportName, strategy = "load") {
|
|
|
193
251
|
type: PROPS_SCRIPT_TYPE,
|
|
194
252
|
htmlFor: id,
|
|
195
253
|
dangerouslySetInnerHTML: { __html: hook.escape(encoded.json) }
|
|
196
|
-
}));
|
|
254
|
+
}), ...promiseBoundaries(encoded.promises, `${exportName} (${src})`));
|
|
197
255
|
}
|
|
198
256
|
Island.displayName = `Island(${Component.displayName || Component.name || exportName})`;
|
|
199
257
|
Object.defineProperty(Island, CLIENT_REFERENCE, { value: {
|
|
@@ -228,6 +286,15 @@ const SERVER_HTML_TAG = HTML_ELEMENT;
|
|
|
228
286
|
*/
|
|
229
287
|
const PAYLOAD_ONLY_ATTRIBUTE = "partial";
|
|
230
288
|
const SERVER_HTML_ATTRIBUTE = "name";
|
|
289
|
+
const OUTLET = Symbol.for("islx.react.outlet");
|
|
290
|
+
/**
|
|
291
|
+
* Where a layout rendered as a server tree places the page (or another outlet, by `name`). The
|
|
292
|
+
* tree records the outlet, and the server and the browser render what they are given there.
|
|
293
|
+
*/
|
|
294
|
+
function Outlet(_props) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
Outlet[OUTLET] = true;
|
|
231
298
|
const PARSER_SENSITIVE_TAGS = /* @__PURE__ */ new Set([
|
|
232
299
|
"caption",
|
|
233
300
|
"colgroup",
|
|
@@ -414,6 +481,10 @@ async function resolveElement(state, element, context) {
|
|
|
414
481
|
i: id
|
|
415
482
|
});
|
|
416
483
|
}
|
|
484
|
+
if (typeof type === "function" && type[OUTLET]) return withKey({
|
|
485
|
+
$: "L",
|
|
486
|
+
n: String(props.name ?? "children")
|
|
487
|
+
});
|
|
417
488
|
const clientReference = getClientReference(type);
|
|
418
489
|
if (clientReference) {
|
|
419
490
|
const { children, ...rest } = props;
|
|
@@ -511,6 +582,7 @@ function createTreeValue(value, resolver) {
|
|
|
511
582
|
fallback: createTreeValue(value.f, resolver)
|
|
512
583
|
}, React.createElement(getChunkComponent(), { chunk: resolver.chunk(value.i) }));
|
|
513
584
|
case "x": return resolver.serverHtml(value.i, typeof value.k === "string" ? value.k : null);
|
|
585
|
+
case "L": return resolver.outlet(value.n);
|
|
514
586
|
case "n": return Number(value.v);
|
|
515
587
|
case "a": return resolver.action(value.v, typeof value.b === "string" ? value.b : void 0);
|
|
516
588
|
case "P": return fulfilledThenable(createTreeValue(value.v, resolver));
|
|
@@ -571,6 +643,22 @@ async function createPayloadOnlyPage(state, payload, encode, resolver) {
|
|
|
571
643
|
return React.createElement(React.Fragment, { key: `c${id}` }, createTreeScript(CHUNK_SCRIPT_TYPE, String(id), chunk.json, chunk.values));
|
|
572
644
|
}));
|
|
573
645
|
}
|
|
646
|
+
/** The name of a tree part: its number, after the tree's scope. */
|
|
647
|
+
function scopedName(scope, id) {
|
|
648
|
+
return scope ? `${scope}:${id}` : String(id);
|
|
649
|
+
}
|
|
650
|
+
function hasChildrenOutlet(value) {
|
|
651
|
+
if (value === null || typeof value !== "object") return false;
|
|
652
|
+
if (Array.isArray(value)) return value.some(hasChildrenOutlet);
|
|
653
|
+
if (value.$ === "L") return value.n === "children";
|
|
654
|
+
return Object.values(value).some(hasChildrenOutlet);
|
|
655
|
+
}
|
|
656
|
+
/** What renders at a layout's outlet: its content, after the layout's payload at `children`. */
|
|
657
|
+
function outletContent(outlets, name, script) {
|
|
658
|
+
const content = outlets?.[name] ?? null;
|
|
659
|
+
if (name !== "children" || !script) return content;
|
|
660
|
+
return React.createElement(React.Fragment, null, script, content);
|
|
661
|
+
}
|
|
574
662
|
/**
|
|
575
663
|
* Resolves a server page and returns what renders in its place: the resolved tree and its
|
|
576
664
|
* payload. Errors from the page's own code (a framework's redirect or not-found) propagate
|
|
@@ -588,6 +676,7 @@ async function renderServerTree(element, options = {}) {
|
|
|
588
676
|
chunks: /* @__PURE__ */ new Map(),
|
|
589
677
|
actions: /* @__PURE__ */ new Map()
|
|
590
678
|
};
|
|
679
|
+
const scope = options.scope;
|
|
591
680
|
const tree = await resolveValue(state, element, { parserSensitive: false }, /* @__PURE__ */ new Set());
|
|
592
681
|
const held = options.held && options.held.size > 0 ? options.held : null;
|
|
593
682
|
const encode = (payload) => {
|
|
@@ -597,6 +686,7 @@ async function renderServerTree(element, options = {}) {
|
|
|
597
686
|
values: addressed.values.map((value) => [value.key, hook.escape(value.json)])
|
|
598
687
|
};
|
|
599
688
|
};
|
|
689
|
+
let layoutScript = null;
|
|
600
690
|
const resolver = {
|
|
601
691
|
component(reference, exportName) {
|
|
602
692
|
const Component = state.components.get(`${reference}#${exportName}`);
|
|
@@ -610,7 +700,7 @@ async function renderServerTree(element, options = {}) {
|
|
|
610
700
|
status: "pending",
|
|
611
701
|
promise: entry.encoded.then((encoded) => {
|
|
612
702
|
const payload = encode({ t: encoded });
|
|
613
|
-
chunk.value = React.createElement(React.Fragment, null, createTreeValue(encoded, resolver), createTreeScript(CHUNK_SCRIPT_TYPE,
|
|
703
|
+
chunk.value = React.createElement(React.Fragment, null, createTreeValue(encoded, resolver), createTreeScript(CHUNK_SCRIPT_TYPE, scopedName(scope, id), payload.json, payload.values));
|
|
614
704
|
chunk.status = "fulfilled";
|
|
615
705
|
return chunk.value;
|
|
616
706
|
}, (error) => {
|
|
@@ -626,10 +716,13 @@ async function renderServerTree(element, options = {}) {
|
|
|
626
716
|
serverHtml(id, key) {
|
|
627
717
|
return React.createElement(SERVER_HTML_TAG, {
|
|
628
718
|
...key === null ? {} : { key },
|
|
629
|
-
[SERVER_HTML_ATTRIBUTE]:
|
|
719
|
+
[SERVER_HTML_ATTRIBUTE]: scopedName(scope, id),
|
|
630
720
|
style: { display: "contents" }
|
|
631
721
|
}, isolate(state.serverHtml.get(id)));
|
|
632
722
|
},
|
|
723
|
+
outlet(name) {
|
|
724
|
+
return outletContent(options.outlets, name, layoutScript);
|
|
725
|
+
},
|
|
633
726
|
action(id) {
|
|
634
727
|
return state.actions.get(id);
|
|
635
728
|
}
|
|
@@ -638,6 +731,16 @@ async function renderServerTree(element, options = {}) {
|
|
|
638
731
|
v: 1,
|
|
639
732
|
t: tree
|
|
640
733
|
});
|
|
734
|
+
if (options.outlets) {
|
|
735
|
+
const script = createTreeScript(TREE_SCRIPT_TYPE, scope ?? "", payload.json, payload.values);
|
|
736
|
+
const atOutlet = hasChildrenOutlet(tree);
|
|
737
|
+
if (atOutlet) layoutScript = script;
|
|
738
|
+
const layout = createTreeValue(tree, resolver);
|
|
739
|
+
const content = atOutlet ? layout : React.createElement(React.Fragment, null, layout, script);
|
|
740
|
+
return React.createElement(function ServerTreeLayout() {
|
|
741
|
+
return content;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
641
744
|
const page = options.payloadOnly && await createPayloadOnlyPage(state, payload, encode, resolver) || createTreePage(tree, payload.json, resolver, void 0, payload.values);
|
|
642
745
|
return React.createElement(function ServerTreePage() {
|
|
643
746
|
return page;
|
|
@@ -673,14 +776,10 @@ function exportOf(module, src, exportName) {
|
|
|
673
776
|
if (typeof Component !== "function" && (typeof Component !== "object" || Component === null)) throw new Error(`${src} has no export ${exportName}`);
|
|
674
777
|
return Component;
|
|
675
778
|
}
|
|
676
|
-
/**
|
|
677
|
-
|
|
678
|
-
* navigation), after loading its client modules. Returns null for a page without a tree.
|
|
679
|
-
*/
|
|
680
|
-
async function createServerTreeElement(source, options) {
|
|
779
|
+
/** Reads a tree's payload, chunks and server HTML from `root` and loads its client modules. */
|
|
780
|
+
async function prepareTree(root, scope, options) {
|
|
681
781
|
const values = options.values ?? getValueStore();
|
|
682
|
-
const
|
|
683
|
-
const script = root.querySelector(`script[type="${TREE_SCRIPT_TYPE}"]`);
|
|
782
|
+
const script = Array.from(root.querySelectorAll(`script[type="${TREE_SCRIPT_TYPE}"]`)).find((candidate) => (candidate.getAttribute("name") ?? void 0) === scope);
|
|
684
783
|
if (!script) return null;
|
|
685
784
|
const json = script.textContent ?? "";
|
|
686
785
|
let ingested = false;
|
|
@@ -696,10 +795,19 @@ async function createServerTreeElement(source, options) {
|
|
|
696
795
|
const payload = parsePayload(json);
|
|
697
796
|
if (payload.v !== 1) throw new Error("unsupported server tree payload version");
|
|
698
797
|
const scripts = readTreeValueScripts(script);
|
|
798
|
+
const partId = (name) => {
|
|
799
|
+
if (name === null) return null;
|
|
800
|
+
const prefix = scope === void 0 ? "" : `${scope}:`;
|
|
801
|
+
if (!name.startsWith(prefix)) return null;
|
|
802
|
+
const id = name.slice(prefix.length);
|
|
803
|
+
return /^\d+$/.test(id) ? Number(id) : null;
|
|
804
|
+
};
|
|
699
805
|
const chunks = /* @__PURE__ */ new Map();
|
|
700
806
|
for (const chunkScript of root.querySelectorAll(`script[type="${CHUNK_SCRIPT_TYPE}"]`)) {
|
|
807
|
+
const id = partId(chunkScript.getAttribute("name"));
|
|
808
|
+
if (id === null) continue;
|
|
701
809
|
const chunkJson = chunkScript.textContent ?? "";
|
|
702
|
-
chunks.set(
|
|
810
|
+
chunks.set(id, {
|
|
703
811
|
tree: parsePayload(chunkJson).t,
|
|
704
812
|
json: chunkJson,
|
|
705
813
|
values: readTreeValueScripts(chunkScript)
|
|
@@ -715,52 +823,84 @@ async function createServerTreeElement(source, options) {
|
|
|
715
823
|
components.set(key, exportOf(await options.load(src), src, key.slice(separator + 1)));
|
|
716
824
|
}));
|
|
717
825
|
const serverHtml = /* @__PURE__ */ new Map();
|
|
718
|
-
for (const container of root.querySelectorAll(`${SERVER_HTML_TAG}[${SERVER_HTML_ATTRIBUTE}]`))
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
826
|
+
for (const container of root.querySelectorAll(`${SERVER_HTML_TAG}[${SERVER_HTML_ATTRIBUTE}]`)) {
|
|
827
|
+
const id = partId(container.getAttribute(SERVER_HTML_ATTRIBUTE));
|
|
828
|
+
if (id !== null) serverHtml.set(id, container.innerHTML);
|
|
829
|
+
}
|
|
830
|
+
const resolver = (outlets, outletScript) => {
|
|
831
|
+
const treeResolver = {
|
|
832
|
+
component(reference, exportName) {
|
|
833
|
+
const Component = components.get(`${reference}#${exportName}`);
|
|
834
|
+
if (!Component) throw new Error(`client reference ${reference}#${exportName} was not loaded`);
|
|
835
|
+
return Component;
|
|
836
|
+
},
|
|
837
|
+
chunk(id) {
|
|
838
|
+
const chunk = chunks.get(id);
|
|
839
|
+
if (!chunk) throw new Error(`server tree chunk ${id} was not found`);
|
|
840
|
+
const value = React.createElement(React.Fragment, null, createTreeValue(chunk.tree, treeResolver), createTreeScript(CHUNK_SCRIPT_TYPE, scopedName(scope, id), chunk.json, chunk.values));
|
|
841
|
+
return {
|
|
842
|
+
status: "fulfilled",
|
|
843
|
+
value,
|
|
844
|
+
promise: Promise.resolve(value)
|
|
845
|
+
};
|
|
846
|
+
},
|
|
847
|
+
serverHtml(id, key) {
|
|
848
|
+
const html = serverHtml.get(id);
|
|
849
|
+
if (html === void 0) throw new Error(`server HTML ${id} was not found`);
|
|
850
|
+
return React.createElement(options.ServerHtml, {
|
|
851
|
+
...key === null ? {} : { key },
|
|
852
|
+
as: SERVER_HTML_TAG,
|
|
853
|
+
[SERVER_HTML_ATTRIBUTE]: scopedName(scope, id),
|
|
854
|
+
style: { display: "contents" },
|
|
855
|
+
suppressHydrationWarning: true,
|
|
856
|
+
dangerouslySetInnerHTML: { __html: html }
|
|
857
|
+
});
|
|
858
|
+
},
|
|
859
|
+
outlet(name) {
|
|
860
|
+
return outletContent(outlets, name, outletScript);
|
|
861
|
+
},
|
|
862
|
+
action(id, sealed) {
|
|
863
|
+
return createServerReference(id, sealed);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
return treeResolver;
|
|
867
|
+
};
|
|
868
|
+
return {
|
|
869
|
+
payload: payload.t,
|
|
870
|
+
json,
|
|
871
|
+
values: scripts,
|
|
872
|
+
components,
|
|
873
|
+
resolver,
|
|
874
|
+
hasOutlet: hasChildrenOutlet(payload.t)
|
|
750
875
|
};
|
|
751
|
-
|
|
876
|
+
}
|
|
877
|
+
/** Points a tree's loaded components at a hot-updated module; true when the tree renders it. */
|
|
878
|
+
function updateTreeComponents(components, src, module) {
|
|
879
|
+
let changed = false;
|
|
880
|
+
for (const key of components.keys()) {
|
|
881
|
+
const separator = key.lastIndexOf("#");
|
|
882
|
+
if (key.slice(0, separator) !== src) continue;
|
|
883
|
+
components.set(key, exportOf(module, src, key.slice(separator + 1)));
|
|
884
|
+
changed = true;
|
|
885
|
+
}
|
|
886
|
+
return changed;
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Rebuilds a server-tree page in the browser from its server HTML (an element, or the HTML of a
|
|
890
|
+
* navigation), after loading its client modules. Returns null for a page without a tree.
|
|
891
|
+
*/
|
|
892
|
+
async function createServerTreeElement(source, options) {
|
|
893
|
+
const prepared = await prepareTree(typeof source === "string" ? parseHtml(source) : source, void 0, options);
|
|
894
|
+
if (!prepared) return null;
|
|
895
|
+
const tree = prepared;
|
|
896
|
+
const resolver = tree.resolver();
|
|
897
|
+
const build = () => createTreePage(tree.payload, tree.json, resolver, void 0, tree.values);
|
|
752
898
|
build();
|
|
753
899
|
function ServerTreePage() {
|
|
754
900
|
const [, setVersion] = React.useState(0);
|
|
755
901
|
React.useEffect(() => {
|
|
756
902
|
const listener = (src, module) => {
|
|
757
|
-
|
|
758
|
-
for (const key of components.keys()) {
|
|
759
|
-
const separator = key.lastIndexOf("#");
|
|
760
|
-
if (key.slice(0, separator) !== src) continue;
|
|
761
|
-
components.set(key, exportOf(module, src, key.slice(separator + 1)));
|
|
762
|
-
changed = true;
|
|
763
|
-
}
|
|
903
|
+
const changed = updateTreeComponents(tree.components, src, module);
|
|
764
904
|
if (changed) setVersion((version) => version + 1);
|
|
765
905
|
return changed;
|
|
766
906
|
};
|
|
@@ -774,4 +914,35 @@ async function createServerTreeElement(source, options) {
|
|
|
774
914
|
}
|
|
775
915
|
return React.createElement(ServerTreePage, options.key === void 0 ? null : { key: options.key });
|
|
776
916
|
}
|
|
777
|
-
|
|
917
|
+
/**
|
|
918
|
+
* Rebuilds a layout the server rendered with `renderServerTree(..., { outlets, scope })`, from
|
|
919
|
+
* its server HTML or a navigation's HTML. The layout's own module never reaches the browser: its
|
|
920
|
+
* client components load from the tree, and `render(outlets)` places the page at its outlet.
|
|
921
|
+
* Rendering it with the same component types at the same place keeps its client state across
|
|
922
|
+
* navigations. Returns null when `source` has no tree for `scope`.
|
|
923
|
+
*/
|
|
924
|
+
async function createServerTreeLayout(source, scope, options) {
|
|
925
|
+
const tree = await prepareTree(typeof source === "string" ? parseHtml(source) : source, scope, options);
|
|
926
|
+
if (!tree) return null;
|
|
927
|
+
const script = createTreeScript(TREE_SCRIPT_TYPE, scope, tree.json, tree.values);
|
|
928
|
+
const render = (outlets) => {
|
|
929
|
+
const resolver = tree.resolver(outlets, tree.hasOutlet ? script : void 0);
|
|
930
|
+
const layout = createTreeValue(tree.payload, resolver);
|
|
931
|
+
return tree.hasOutlet ? layout : React.createElement(React.Fragment, null, layout, script);
|
|
932
|
+
};
|
|
933
|
+
render({});
|
|
934
|
+
const listener = (src, module) => {
|
|
935
|
+
const changed = updateTreeComponents(tree.components, src, module);
|
|
936
|
+
if (changed) options.onUpdate?.();
|
|
937
|
+
return changed;
|
|
938
|
+
};
|
|
939
|
+
const listeners = treeListeners();
|
|
940
|
+
listeners.add(listener);
|
|
941
|
+
return {
|
|
942
|
+
render,
|
|
943
|
+
dispose: () => {
|
|
944
|
+
listeners.delete(listener);
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
export { renderServerTree as a, createIsland as c, isolateIslands as d, runtimeTagged as f, withinClientRoot as h, createServerTreeLayout as i, getClientReference as l, setRenderStateResolver as m, PAYLOAD_ONLY_ATTRIBUTE as n, updateServerTrees as o, setIslandServerHook as p, createServerTreeElement as r, IslandsProvider as s, Outlet as t, getContexts as u };
|
|
@@ -24,6 +24,11 @@ declare const TREE_SCRIPT_TYPE = "f/tree";
|
|
|
24
24
|
declare const CHUNK_SCRIPT_TYPE = "f/chunk";
|
|
25
25
|
/** The element around server HTML inside a server tree. */
|
|
26
26
|
declare const HTML_ELEMENT = "f-html";
|
|
27
|
+
/**
|
|
28
|
+
* The script type of a promise's outcome, written when it settles: `{"v":value}`, or `{"e":1}` for
|
|
29
|
+
* a rejection. Its `name` attribute names the promise.
|
|
30
|
+
*/
|
|
31
|
+
declare const PROMISE_SCRIPT_TYPE = "f/promise";
|
|
27
32
|
/** The script type of React's form state after a `useActionState` form post, for hydration. */
|
|
28
33
|
declare const FORM_STATE_SCRIPT_TYPE = "f/form-state";
|
|
29
34
|
/** The reserved JSON key whose value is a tag. */
|
|
@@ -38,6 +43,11 @@ declare const REFERENCE_TAG = "r";
|
|
|
38
43
|
declare const SLOT_TAG = "s";
|
|
39
44
|
/** `{"$":"a","v":id}` or `{"$":"a","v":id,"b":sealed}`: a server action. */
|
|
40
45
|
declare const ACTION_TAG = "a";
|
|
46
|
+
/**
|
|
47
|
+
* `{"$":"T","v":name}`: a promise an island receives, settled by the `f/promise` script `name`.
|
|
48
|
+
* (Inside a server tree, `{"$":"P","v":value}` is a promise that resolved already.)
|
|
49
|
+
*/
|
|
50
|
+
declare const PROMISE_TAG = "T";
|
|
41
51
|
/** `{"$":"o","v":{…}}`: app data that has its own `$` key. */
|
|
42
52
|
declare const OBJECT_TAG = "o";
|
|
43
53
|
/** `{"$":"F","v":n}`: a `FormData` argument, sent as fields of a multipart call. */
|
|
@@ -95,4 +105,4 @@ declare const MAX_HELD_VALUES = 128;
|
|
|
95
105
|
* `{"$":"a","v":id,"b":sealed}`), or undefined for anything else.
|
|
96
106
|
*/
|
|
97
107
|
declare function getTag(value: unknown): string | undefined;
|
|
98
|
-
export {
|
|
108
|
+
export { VALUES_HEADER as A, RENDER_HEADER as C, TAG_BOUND as D, TAG as E, getTag as F, readPostedAction as I, stripRuntimeFields as L, VALUE_SCRIPT_TYPE as M, attachmentField as N, TAG_VALUE as O, formFieldName as P, REFERENCE_TAG as S, SLOT_TAG as T, MAX_HELD_VALUES as _, BLOB_TAG as a, PROMISE_TAG as b, CHUNK_SCRIPT_TYPE as c, FORM_REF_PREFIX as d, FORM_STATE_SCRIPT_TYPE as f, LAYOUTS_HEADER as g, ISLAND_ELEMENT as h, ACTION_TAG as i, VALUE_KEY_PATTERN as j, TREE_SCRIPT_TYPE as k, FORM_DATA_TAG as l, HYDRATED_ATTRIBUTE as m, ACTION_KEY_FIELD as n, BOUND_FIELD as o, HTML_ELEMENT as p, ACTION_STALE_HEADER as r, CALL_FIELD as s, ACTION_HEADER as t, FORM_FIELD_PREFIX as u, OBJECT_TAG as v, SLOT_ELEMENT as w, PROPS_SCRIPT_TYPE as x, PROMISE_SCRIPT_TYPE as y };
|
|
@@ -25,6 +25,11 @@ const TREE_SCRIPT_TYPE = "f/tree";
|
|
|
25
25
|
const CHUNK_SCRIPT_TYPE = "f/chunk";
|
|
26
26
|
/** The element around server HTML inside a server tree. */
|
|
27
27
|
const HTML_ELEMENT = "f-html";
|
|
28
|
+
/**
|
|
29
|
+
* The script type of a promise's outcome, written when it settles: `{"v":value}`, or `{"e":1}` for
|
|
30
|
+
* a rejection. Its `name` attribute names the promise.
|
|
31
|
+
*/
|
|
32
|
+
const PROMISE_SCRIPT_TYPE = "f/promise";
|
|
28
33
|
/** The script type of React's form state after a `useActionState` form post, for hydration. */
|
|
29
34
|
const FORM_STATE_SCRIPT_TYPE = "f/form-state";
|
|
30
35
|
/** The reserved JSON key whose value is a tag. */
|
|
@@ -39,6 +44,11 @@ const REFERENCE_TAG = "r";
|
|
|
39
44
|
const SLOT_TAG = "s";
|
|
40
45
|
/** `{"$":"a","v":id}` or `{"$":"a","v":id,"b":sealed}`: a server action. */
|
|
41
46
|
const ACTION_TAG = "a";
|
|
47
|
+
/**
|
|
48
|
+
* `{"$":"T","v":name}`: a promise an island receives, settled by the `f/promise` script `name`.
|
|
49
|
+
* (Inside a server tree, `{"$":"P","v":value}` is a promise that resolved already.)
|
|
50
|
+
*/
|
|
51
|
+
const PROMISE_TAG = "T";
|
|
42
52
|
/** `{"$":"o","v":{…}}`: app data that has its own `$` key. */
|
|
43
53
|
const OBJECT_TAG = "o";
|
|
44
54
|
/** `{"$":"F","v":n}`: a `FormData` argument, sent as fields of a multipart call. */
|
|
@@ -118,4 +128,4 @@ function getTag(value) {
|
|
|
118
128
|
if (keys.length === 2 && "v" in value) return tag;
|
|
119
129
|
if (keys.length === 3 && tag === "a" && "v" in value && "b" in value) return tag;
|
|
120
130
|
}
|
|
121
|
-
export {
|
|
131
|
+
export { VALUES_HEADER as A, RENDER_HEADER as C, TAG_BOUND as D, TAG as E, getTag as F, readPostedAction as I, stripRuntimeFields as L, VALUE_SCRIPT_TYPE as M, attachmentField as N, TAG_VALUE as O, formFieldName as P, REFERENCE_TAG as S, SLOT_TAG as T, MAX_HELD_VALUES as _, BLOB_TAG as a, PROMISE_TAG as b, CHUNK_SCRIPT_TYPE as c, FORM_REF_PREFIX as d, FORM_STATE_SCRIPT_TYPE as f, LAYOUTS_HEADER as g, ISLAND_ELEMENT as h, ACTION_TAG as i, VALUE_KEY_PATTERN as j, TREE_SCRIPT_TYPE as k, FORM_DATA_TAG as l, HYDRATED_ATTRIBUTE as m, ACTION_KEY_FIELD as n, BOUND_FIELD as o, HTML_ELEMENT as p, ACTION_STALE_HEADER as r, CALL_FIELD as s, ACTION_HEADER as t, FORM_FIELD_PREFIX as u, OBJECT_TAG as v, SLOT_ELEMENT as w, PROPS_SCRIPT_TYPE as x, PROMISE_SCRIPT_TYPE as y };
|
package/dist/client.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as emit, c as createValueStore, i as defineClientHooks, l as getValueStore, n as decodeActionReference, o as hookEventName, r as setActionNavigator, s as setClientHooks, t as createServerReference, u as valueHeaders } from "./_chunks/actions-
|
|
2
|
-
import {
|
|
1
|
+
import { a as emit, c as createValueStore, i as defineClientHooks, l as getValueStore, n as decodeActionReference, o as hookEventName, r as setActionNavigator, s as setClientHooks, t as createServerReference, u as valueHeaders } from "./_chunks/actions-8RQfrM-p.mjs";
|
|
2
|
+
import { C as RENDER_HEADER, I as readPostedAction, L as stripRuntimeFields } from "./_chunks/wire-QEQWBr5s.mjs";
|
|
3
3
|
import { i as REPLAYABLE_SELECTOR, n as PENDING_INTERACTIONS, r as PREHYDRATION_SCRIPT, t as INTERACTION_EVENT } from "./_chunks/prehydration-B1GLTLdk.mjs";
|
|
4
4
|
// src/client/forms.ts
|
|
5
5
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as VALUES_HEADER, C as RENDER_HEADER, D as TAG_BOUND, E as TAG, F as getTag, I as readPostedAction, L as stripRuntimeFields, M as VALUE_SCRIPT_TYPE, N as attachmentField, O as TAG_VALUE, P as formFieldName, S as REFERENCE_TAG, T as SLOT_TAG, _ as MAX_HELD_VALUES, a as BLOB_TAG, b as PROMISE_TAG, c as CHUNK_SCRIPT_TYPE, d as FORM_REF_PREFIX, f as FORM_STATE_SCRIPT_TYPE, g as LAYOUTS_HEADER, h as ISLAND_ELEMENT, i as ACTION_TAG, j as VALUE_KEY_PATTERN, k as TREE_SCRIPT_TYPE, l as FORM_DATA_TAG, m as HYDRATED_ATTRIBUTE, n as ACTION_KEY_FIELD, o as BOUND_FIELD, p as HTML_ELEMENT, r as ACTION_STALE_HEADER, s as CALL_FIELD, t as ACTION_HEADER, u as FORM_FIELD_PREFIX, v as OBJECT_TAG, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "./_chunks/wire-Dipqh-EB.mjs";
|
|
2
2
|
import { a as decode, c as encodeLeaf, d as plainObject, f as tagged, i as NOT_A_VALUE, l as fromBase64, n as EncodeError, o as decodeTagged, p as toBase64, r as EncodeOptions, s as encode, t as DecodeOptions, u as needsObjectTag } from "./_chunks/codec-CK8kZp4u.mjs";
|
|
3
|
-
export { ACTION_HEADER, ACTION_KEY_FIELD, ACTION_STALE_HEADER, ACTION_TAG, BLOB_TAG, BOUND_FIELD, CALL_FIELD, CHUNK_SCRIPT_TYPE, type DecodeOptions, EncodeError, type EncodeOptions, FORM_DATA_TAG, FORM_FIELD_PREFIX, FORM_REF_PREFIX, FORM_STATE_SCRIPT_TYPE, HTML_ELEMENT, HYDRATED_ATTRIBUTE, ISLAND_ELEMENT, LAYOUTS_HEADER, MAX_HELD_VALUES, NOT_A_VALUE, OBJECT_TAG, PROPS_SCRIPT_TYPE, REFERENCE_TAG, RENDER_HEADER, SLOT_ELEMENT, SLOT_TAG, TAG, TAG_BOUND, TAG_VALUE, TREE_SCRIPT_TYPE, VALUES_HEADER, VALUE_KEY_PATTERN, VALUE_SCRIPT_TYPE, attachmentField, decode, decodeTagged, encode, encodeLeaf, formFieldName, fromBase64, getTag, needsObjectTag, plainObject, readPostedAction, stripRuntimeFields, tagged, toBase64 };
|
|
3
|
+
export { ACTION_HEADER, ACTION_KEY_FIELD, ACTION_STALE_HEADER, ACTION_TAG, BLOB_TAG, BOUND_FIELD, CALL_FIELD, CHUNK_SCRIPT_TYPE, type DecodeOptions, EncodeError, type EncodeOptions, FORM_DATA_TAG, FORM_FIELD_PREFIX, FORM_REF_PREFIX, FORM_STATE_SCRIPT_TYPE, HTML_ELEMENT, HYDRATED_ATTRIBUTE, ISLAND_ELEMENT, LAYOUTS_HEADER, MAX_HELD_VALUES, NOT_A_VALUE, OBJECT_TAG, PROMISE_SCRIPT_TYPE, PROMISE_TAG, PROPS_SCRIPT_TYPE, REFERENCE_TAG, RENDER_HEADER, SLOT_ELEMENT, SLOT_TAG, TAG, TAG_BOUND, TAG_VALUE, TREE_SCRIPT_TYPE, VALUES_HEADER, VALUE_KEY_PATTERN, VALUE_SCRIPT_TYPE, attachmentField, decode, decodeTagged, encode, encodeLeaf, formFieldName, fromBase64, getTag, needsObjectTag, plainObject, readPostedAction, stripRuntimeFields, tagged, toBase64 };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { a as encode, c as needsObjectTag, d as tagged, f as toBase64, i as decodeTagged, l as plainObject, n as NOT_A_VALUE, o as encodeLeaf, r as decode, s as fromBase64, t as EncodeError } from "./_chunks/codec-
|
|
3
|
-
export { ACTION_HEADER, ACTION_KEY_FIELD, ACTION_STALE_HEADER, ACTION_TAG, BLOB_TAG, BOUND_FIELD, CALL_FIELD, CHUNK_SCRIPT_TYPE, EncodeError, FORM_DATA_TAG, FORM_FIELD_PREFIX, FORM_REF_PREFIX, FORM_STATE_SCRIPT_TYPE, HTML_ELEMENT, HYDRATED_ATTRIBUTE, ISLAND_ELEMENT, LAYOUTS_HEADER, MAX_HELD_VALUES, NOT_A_VALUE, OBJECT_TAG, PROPS_SCRIPT_TYPE, REFERENCE_TAG, RENDER_HEADER, SLOT_ELEMENT, SLOT_TAG, TAG, TAG_BOUND, TAG_VALUE, TREE_SCRIPT_TYPE, VALUES_HEADER, VALUE_KEY_PATTERN, VALUE_SCRIPT_TYPE, attachmentField, decode, decodeTagged, encode, encodeLeaf, formFieldName, fromBase64, getTag, needsObjectTag, plainObject, readPostedAction, stripRuntimeFields, tagged, toBase64 };
|
|
1
|
+
import { A as VALUES_HEADER, C as RENDER_HEADER, D as TAG_BOUND, E as TAG, F as getTag, I as readPostedAction, L as stripRuntimeFields, M as VALUE_SCRIPT_TYPE, N as attachmentField, O as TAG_VALUE, P as formFieldName, S as REFERENCE_TAG, T as SLOT_TAG, _ as MAX_HELD_VALUES, a as BLOB_TAG, b as PROMISE_TAG, c as CHUNK_SCRIPT_TYPE, d as FORM_REF_PREFIX, f as FORM_STATE_SCRIPT_TYPE, g as LAYOUTS_HEADER, h as ISLAND_ELEMENT, i as ACTION_TAG, j as VALUE_KEY_PATTERN, k as TREE_SCRIPT_TYPE, l as FORM_DATA_TAG, m as HYDRATED_ATTRIBUTE, n as ACTION_KEY_FIELD, o as BOUND_FIELD, p as HTML_ELEMENT, r as ACTION_STALE_HEADER, s as CALL_FIELD, t as ACTION_HEADER, u as FORM_FIELD_PREFIX, v as OBJECT_TAG, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "./_chunks/wire-QEQWBr5s.mjs";
|
|
2
|
+
import { a as encode, c as needsObjectTag, d as tagged, f as toBase64, i as decodeTagged, l as plainObject, n as NOT_A_VALUE, o as encodeLeaf, r as decode, s as fromBase64, t as EncodeError } from "./_chunks/codec-C8to-RQW.mjs";
|
|
3
|
+
export { ACTION_HEADER, ACTION_KEY_FIELD, ACTION_STALE_HEADER, ACTION_TAG, BLOB_TAG, BOUND_FIELD, CALL_FIELD, CHUNK_SCRIPT_TYPE, EncodeError, FORM_DATA_TAG, FORM_FIELD_PREFIX, FORM_REF_PREFIX, FORM_STATE_SCRIPT_TYPE, HTML_ELEMENT, HYDRATED_ATTRIBUTE, ISLAND_ELEMENT, LAYOUTS_HEADER, MAX_HELD_VALUES, NOT_A_VALUE, OBJECT_TAG, PROMISE_SCRIPT_TYPE, PROMISE_TAG, PROPS_SCRIPT_TYPE, REFERENCE_TAG, RENDER_HEADER, SLOT_ELEMENT, SLOT_TAG, TAG, TAG_BOUND, TAG_VALUE, TREE_SCRIPT_TYPE, VALUES_HEADER, VALUE_KEY_PATTERN, VALUE_SCRIPT_TYPE, attachmentField, decode, decodeTagged, encode, encodeLeaf, formFieldName, fromBase64, getTag, needsObjectTag, plainObject, readPostedAction, stripRuntimeFields, tagged, toBase64 };
|
package/dist/react/client.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as scheduleHydration, i as isIslandStrategy, n as IslandStrategy, r as ScheduleOptions, t as ISLAND_STRATEGIES } from "../_chunks/schedule-Dw14dW0H.mjs";
|
|
2
|
-
import { m as HYDRATED_ATTRIBUTE } from "../_chunks/wire-
|
|
2
|
+
import { m as HYDRATED_ATTRIBUTE } from "../_chunks/wire-Dipqh-EB.mjs";
|
|
3
3
|
import { t as ValueStore } from "../_chunks/value-store-Dm-7xJ6W.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { a as ServerTreeLayoutOptions, c as createServerTreeLayout, g as isolateIslands, i as ServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, r as ServerTreeElementOptions, s as createServerTreeElement, u as updateServerTrees, v as withinClientRoot } from "../_chunks/tree-B4RMU2UX.mjs";
|
|
5
5
|
import * as React from "react";
|
|
6
6
|
|
|
7
7
|
// src/react/hydrate.d.ts
|
|
@@ -21,6 +21,12 @@ interface HydrationOptions {
|
|
|
21
21
|
*/
|
|
22
22
|
schedule: typeof scheduleHydration;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Marks the Suspense boundaries under `root` that still wait once their stream has ended (the
|
|
26
|
+
* connection dropped) as rendered in the browser, as React does when it finds one: the next
|
|
27
|
+
* `hydrate()` pass starts their islands. Call it when a stream that filled `root` ends.
|
|
28
|
+
*/
|
|
29
|
+
declare function endStreamedContent(root: ParentNode): void;
|
|
24
30
|
/** The hydration runtime for one page. */
|
|
25
31
|
declare function createHydrationRuntime(options: HydrationOptions): {
|
|
26
32
|
hydrate: (scope?: ParentNode, signal?: AbortSignal) => Promise<void>;
|
|
@@ -46,4 +52,4 @@ declare function createServerHtml(runtime: Pick<HydrationRuntime, "hydrate" | "d
|
|
|
46
52
|
declare function refreshDocument(runtime: HydrationRuntime): Promise<void>;
|
|
47
53
|
/** Puts another page's body in place of this one's, and hydrates its islands. */
|
|
48
54
|
declare function swapDocument(runtime: HydrationRuntime, next: Document): Promise<void>;
|
|
49
|
-
export { HYDRATED_ATTRIBUTE, type HydrationOptions, type HydrationRuntime, ISLAND_STRATEGIES, type IslandStrategy, PAYLOAD_ONLY_ATTRIBUTE, type ScheduleOptions, type ServerTreeElementOptions, createHydrationRuntime, createServerHtml, createServerTreeElement, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
|
|
55
|
+
export { HYDRATED_ATTRIBUTE, type HydrationOptions, type HydrationRuntime, ISLAND_STRATEGIES, type IslandStrategy, PAYLOAD_ONLY_ATTRIBUTE, type ScheduleOptions, type ServerTreeElementOptions, type ServerTreeLayout, type ServerTreeLayoutOptions, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, endStreamedContent, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
|
package/dist/react/client.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-
|
|
2
|
-
import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-
|
|
1
|
+
import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-8RQfrM-p.mjs";
|
|
2
|
+
import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-C8to-RQW.mjs";
|
|
3
|
+
import { F as getTag, f as FORM_STATE_SCRIPT_TYPE, h as ISLAND_ELEMENT, m as HYDRATED_ATTRIBUTE, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
|
|
4
|
+
import { d as isolateIslands, h as withinClientRoot, i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement, u as getContexts } from "../_chunks/tree-opIpaCke.mjs";
|
|
5
|
+
import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-CjoybS3b.mjs";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import { hydrateRoot } from "react-dom/client";
|
|
8
8
|
// src/react/hydrate.ts
|
|
@@ -39,22 +39,22 @@ function readFormState(document) {
|
|
|
39
39
|
const revivedValues = /* @__PURE__ */ new WeakMap();
|
|
40
40
|
let slotsRevived = 0;
|
|
41
41
|
/** Turns travelled props back into values: slots keep their server HTML, actions become callable. */
|
|
42
|
-
function revive(value, container) {
|
|
42
|
+
function revive(value, container, values) {
|
|
43
43
|
if (value && typeof value === "object") {
|
|
44
44
|
const cached = revivedValues.get(value);
|
|
45
45
|
if (cached !== void 0) return cached;
|
|
46
46
|
const slotsBefore = slotsRevived;
|
|
47
|
-
const revived = reviveUncached(value, container);
|
|
47
|
+
const revived = reviveUncached(value, container, values);
|
|
48
48
|
if (slotsRevived === slotsBefore) revivedValues.set(value, revived);
|
|
49
49
|
return revived;
|
|
50
50
|
}
|
|
51
51
|
return value;
|
|
52
52
|
}
|
|
53
|
-
function reviveUncached(value, container) {
|
|
53
|
+
function reviveUncached(value, container, values) {
|
|
54
54
|
if (Array.isArray(value)) {
|
|
55
55
|
let changed = false;
|
|
56
56
|
const revived = value.map((entry) => {
|
|
57
|
-
const next = revive(entry, container);
|
|
57
|
+
const next = revive(entry, container, values);
|
|
58
58
|
if (next !== entry) changed = true;
|
|
59
59
|
return next;
|
|
60
60
|
});
|
|
@@ -82,25 +82,152 @@ function reviveUncached(value, container) {
|
|
|
82
82
|
dangerouslySetInnerHTML: { __html: html }
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
|
+
if (tag === "T" && typeof object["v"] === "string") return islandPromise(object["v"], container, values);
|
|
85
86
|
if (tag === "r") throw new Error(`islx value ${String(object["v"])} was not resolved`);
|
|
86
87
|
if (tag !== void 0) {
|
|
87
|
-
const decoded = decodeTagged(object, (child) => revive(child, container));
|
|
88
|
+
const decoded = decodeTagged(object, (child) => revive(child, container, values));
|
|
88
89
|
if (decoded !== NOT_A_VALUE) return decoded;
|
|
89
90
|
}
|
|
90
91
|
let changed = false;
|
|
91
92
|
const revived = {};
|
|
92
93
|
for (const [key, entry] of Object.entries(object)) {
|
|
93
|
-
const next = revive(entry, container);
|
|
94
|
+
const next = revive(entry, container, values);
|
|
94
95
|
setOwn(revived, key, next);
|
|
95
96
|
if (next !== entry) changed = true;
|
|
96
97
|
}
|
|
97
98
|
return changed ? revived : value;
|
|
98
99
|
}
|
|
100
|
+
const pendingPromises = /* @__PURE__ */ new Map();
|
|
101
|
+
let promiseObserver = null;
|
|
102
|
+
function findPromiseScript(container, name) {
|
|
103
|
+
const documents = new Set([container.ownerDocument, globalThis.document].filter(Boolean));
|
|
104
|
+
for (const document of documents) for (const script of document.querySelectorAll(`script[type="${PROMISE_SCRIPT_TYPE}"]`)) if (script.getAttribute("name") === name) return script;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function readOutcome(script, container, values) {
|
|
108
|
+
try {
|
|
109
|
+
const text = script.textContent || "{}";
|
|
110
|
+
let parsed = JSON.parse(text);
|
|
111
|
+
if (!parsed || typeof parsed !== "object" || !("v" in parsed)) return {
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: /* @__PURE__ */ new Error("islx: the promise rejected on the server")
|
|
114
|
+
};
|
|
115
|
+
if (text.includes(`"\$":"r"`)) {
|
|
116
|
+
values.ingest(container.ownerDocument);
|
|
117
|
+
parsed = values.resolve(parsed);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
value: revive(parsed, container, values).v
|
|
122
|
+
};
|
|
123
|
+
} catch (reason) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
reason
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The promise an island received, as a thenable React's `use` reads: settled already when its
|
|
132
|
+
* outcome is on the page, so hydration does not suspend, and settled when the outcome streams in
|
|
133
|
+
* otherwise.
|
|
134
|
+
*/
|
|
135
|
+
function islandPromise(name, container, values) {
|
|
136
|
+
const pending = pendingPromises.get(name);
|
|
137
|
+
if (pending) return pending.thenable;
|
|
138
|
+
const script = findPromiseScript(container, name);
|
|
139
|
+
if (script) {
|
|
140
|
+
const outcome = readOutcome(script, container, values);
|
|
141
|
+
const thenable = outcome.ok ? Promise.resolve(outcome.value) : Promise.reject(outcome.reason);
|
|
142
|
+
if (outcome.ok) Object.assign(thenable, {
|
|
143
|
+
status: "fulfilled",
|
|
144
|
+
value: outcome.value
|
|
145
|
+
});
|
|
146
|
+
else {
|
|
147
|
+
thenable.catch(() => {});
|
|
148
|
+
Object.assign(thenable, {
|
|
149
|
+
status: "rejected",
|
|
150
|
+
reason: outcome.reason
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return thenable;
|
|
154
|
+
}
|
|
155
|
+
let resolve;
|
|
156
|
+
let reject;
|
|
157
|
+
const thenable = new Promise((done, fail) => {
|
|
158
|
+
resolve = done;
|
|
159
|
+
reject = fail;
|
|
160
|
+
});
|
|
161
|
+
thenable.catch(() => {});
|
|
162
|
+
thenable.status = "pending";
|
|
163
|
+
pendingPromises.set(name, {
|
|
164
|
+
thenable,
|
|
165
|
+
container,
|
|
166
|
+
values,
|
|
167
|
+
settle(found) {
|
|
168
|
+
pendingPromises.delete(name);
|
|
169
|
+
const outcome = readOutcome(found, container, values);
|
|
170
|
+
if (outcome.ok) {
|
|
171
|
+
Object.assign(thenable, {
|
|
172
|
+
status: "fulfilled",
|
|
173
|
+
value: outcome.value
|
|
174
|
+
});
|
|
175
|
+
resolve(outcome.value);
|
|
176
|
+
} else {
|
|
177
|
+
Object.assign(thenable, {
|
|
178
|
+
status: "rejected",
|
|
179
|
+
reason: outcome.reason
|
|
180
|
+
});
|
|
181
|
+
reject(outcome.reason);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
if (!promiseObserver && typeof MutationObserver === "function") {
|
|
186
|
+
promiseObserver = new MutationObserver(() => {
|
|
187
|
+
for (const [pendingName, entry] of pendingPromises) {
|
|
188
|
+
const found = findPromiseScript(entry.container, pendingName);
|
|
189
|
+
if (found) entry.settle(found);
|
|
190
|
+
}
|
|
191
|
+
if (pendingPromises.size === 0) {
|
|
192
|
+
promiseObserver?.disconnect();
|
|
193
|
+
promiseObserver = null;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
promiseObserver.observe(globalThis.document ?? container.ownerDocument, {
|
|
197
|
+
childList: true,
|
|
198
|
+
subtree: true
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return thenable;
|
|
202
|
+
}
|
|
99
203
|
/**
|
|
100
204
|
* React streams Suspense content that resolves late into a hidden `<div id="S:n">`, then moves it
|
|
101
205
|
* into place. Its islands hydrate once moved, not while staged.
|
|
102
206
|
*/
|
|
103
207
|
const STREAM_STAGING = "div[hidden][id^=\"S:\"]";
|
|
208
|
+
const PENDING_BOUNDARY = "$?";
|
|
209
|
+
/**
|
|
210
|
+
* Whether React still streams Suspense content into `container`. React waits for such a boundary
|
|
211
|
+
* only while the document loads; later (a navigation that streams) it would render it again in
|
|
212
|
+
* the browser, so the island hydrates once React has revealed it.
|
|
213
|
+
*/
|
|
214
|
+
function awaitsStreamedContent(container) {
|
|
215
|
+
const document = container.ownerDocument;
|
|
216
|
+
if (document.readyState === "loading") return false;
|
|
217
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT);
|
|
218
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) if (node.data === PENDING_BOUNDARY) return true;
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Marks the Suspense boundaries under `root` that still wait once their stream has ended (the
|
|
223
|
+
* connection dropped) as rendered in the browser, as React does when it finds one: the next
|
|
224
|
+
* `hydrate()` pass starts their islands. Call it when a stream that filled `root` ends.
|
|
225
|
+
*/
|
|
226
|
+
function endStreamedContent(root) {
|
|
227
|
+
const node = root;
|
|
228
|
+
const walker = (node.nodeType === 9 ? node : node.ownerDocument).createTreeWalker(node, NodeFilter.SHOW_COMMENT);
|
|
229
|
+
for (let comment = walker.nextNode(); comment; comment = walker.nextNode()) if (comment.data === PENDING_BOUNDARY) comment.data = "$!";
|
|
230
|
+
}
|
|
104
231
|
/** The hydration runtime for one page. */
|
|
105
232
|
function createHydrationRuntime(options) {
|
|
106
233
|
const roots = /* @__PURE__ */ new Map();
|
|
@@ -169,7 +296,7 @@ function createHydrationRuntime(options) {
|
|
|
169
296
|
values.ingest(container.ownerDocument);
|
|
170
297
|
parsed = values.resolve(parsed);
|
|
171
298
|
}
|
|
172
|
-
return revive(parsed, container);
|
|
299
|
+
return revive(parsed, container, values);
|
|
173
300
|
}
|
|
174
301
|
const topLevel = (scope) => Array.from(scope.querySelectorAll(ISLAND_SELECTOR)).filter((container) => !container.parentElement?.closest(ISLAND_SELECTOR));
|
|
175
302
|
const keyOf = (container) => `${container.getAttribute("src")}#${container.getAttribute("export") || "default"}`;
|
|
@@ -182,7 +309,7 @@ function createHydrationRuntime(options) {
|
|
|
182
309
|
if (scope instanceof Document) watchStream(scope);
|
|
183
310
|
const candidates = Array.from(scope.querySelectorAll(ISLAND_SELECTOR));
|
|
184
311
|
if (scope instanceof Element && scope.matches(ISLAND_SELECTOR)) candidates.unshift(scope);
|
|
185
|
-
const islands = candidates.filter((container) => !roots.has(container) && !pending.has(container) && !container.parentElement?.closest(ISLAND_SELECTOR) && !container.closest(STREAM_STAGING));
|
|
312
|
+
const islands = candidates.filter((container) => !roots.has(container) && !pending.has(container) && !container.parentElement?.closest(ISLAND_SELECTOR) && !container.closest(STREAM_STAGING) && !awaitsStreamedContent(container));
|
|
186
313
|
await Promise.all(islands.map(async (container) => {
|
|
187
314
|
const src = container.getAttribute("src");
|
|
188
315
|
const id = container.getAttribute("props");
|
|
@@ -350,6 +477,7 @@ function createHydrationRuntime(options) {
|
|
|
350
477
|
if (document.readyState !== "loading" && !document.querySelector(STREAM_STAGING)) {
|
|
351
478
|
observer.disconnect();
|
|
352
479
|
watching = false;
|
|
480
|
+
endStreamedContent(document);
|
|
353
481
|
}
|
|
354
482
|
hydrate(document).catch((error) => report("Hydrating streamed islands failed.", error));
|
|
355
483
|
};
|
|
@@ -366,6 +494,11 @@ function createHydrationRuntime(options) {
|
|
|
366
494
|
document.addEventListener("DOMContentLoaded", run, { once: true });
|
|
367
495
|
}
|
|
368
496
|
function dispose(scope) {
|
|
497
|
+
for (const [name, entry] of pendingPromises) if (scope.contains(entry.container)) pendingPromises.delete(name);
|
|
498
|
+
if (pendingPromises.size === 0) {
|
|
499
|
+
promiseObserver?.disconnect();
|
|
500
|
+
promiseObserver = null;
|
|
501
|
+
}
|
|
369
502
|
for (const [container, controller] of pending) if (container === scope || scope.contains(container)) {
|
|
370
503
|
controller.abort();
|
|
371
504
|
pending.delete(container);
|
|
@@ -453,4 +586,4 @@ async function swapDocument(runtime, next) {
|
|
|
453
586
|
document.body.replaceChildren(...Array.from(body.childNodes));
|
|
454
587
|
await runtime.hydrate(document);
|
|
455
588
|
}
|
|
456
|
-
export { HYDRATED_ATTRIBUTE, ISLAND_STRATEGIES, PAYLOAD_ONLY_ATTRIBUTE, createHydrationRuntime, createServerHtml, createServerTreeElement, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
|
|
589
|
+
export { HYDRATED_ATTRIBUTE, ISLAND_STRATEGIES, PAYLOAD_ONLY_ATTRIBUTE, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, endStreamedContent, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
|
package/dist/react/server.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as ReactFormState } from "../_chunks/handle-C0N9ZyNV.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { _ as setRenderStateResolver, d as ClientReference, f as IslandRenderState, g as isolateIslands, h as getClientReference, l as renderServerTree, m as createIsland, n as PAYLOAD_ONLY_ATTRIBUTE, o as ServerTreeRenderOptions, p as IslandsProvider, t as Outlet, v as withinClientRoot } from "../_chunks/tree-B4RMU2UX.mjs";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { RenderToReadableStreamOptions } from "react-dom/server";
|
|
5
5
|
|
|
@@ -48,4 +48,4 @@ declare function renderToStream(element: React.ReactNode, {
|
|
|
48
48
|
}>;
|
|
49
49
|
/** Whether a response rendered for `request` leaves out values its browser holds. */
|
|
50
50
|
declare function isPersonalized(request: Request): boolean;
|
|
51
|
-
export { type ClientReference, type IslandRenderState, IslandsProvider, PAYLOAD_ONLY_ATTRIBUTE, RenderToStreamOptions, type ServerTreeRenderOptions, createIsland, getClientReference, isPersonalized, isolateIslands, renderServerTree, renderToStream, setRenderStateResolver, withinClientRoot };
|
|
51
|
+
export { type ClientReference, type IslandRenderState, IslandsProvider, Outlet, PAYLOAD_ONLY_ATTRIBUTE, RenderToStreamOptions, type ServerTreeRenderOptions, createIsland, getClientReference, isPersonalized, isolateIslands, renderServerTree, renderToStream, setRenderStateResolver, withinClientRoot };
|
package/dist/react/server.mjs
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
|
-
import { a as encode } from "../_chunks/codec-
|
|
2
|
-
import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-
|
|
1
|
+
import { a as encode } from "../_chunks/codec-C8to-RQW.mjs";
|
|
2
|
+
import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
|
|
3
3
|
import { r as PREHYDRATION_SCRIPT } from "../_chunks/prehydration-B1GLTLdk.mjs";
|
|
4
|
-
import { g as escapeScriptJson, h as addressValues, i as encodeActionReference, v as parseHeldValues, y as selectValuesToSend } from "../_chunks/actions-
|
|
4
|
+
import { g as escapeScriptJson, h as addressValues, i as encodeActionReference, v as parseHeldValues, y as selectValuesToSend } from "../_chunks/actions-CtLpJPEB.mjs";
|
|
5
5
|
import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
|
|
6
|
-
import { a as
|
|
6
|
+
import { a as renderServerTree, c as createIsland, d as isolateIslands, f as runtimeTagged, h as withinClientRoot, l as getClientReference, m as setRenderStateResolver, n as PAYLOAD_ONLY_ATTRIBUTE, p as setIslandServerHook, s as IslandsProvider, t as Outlet, u as getContexts } from "../_chunks/tree-opIpaCke.mjs";
|
|
7
7
|
import * as React from "react";
|
|
8
8
|
// src/react/server.ts
|
|
9
9
|
/**
|
|
10
10
|
* @farm.js/islx/react/server: rendering a React page with islands. Importing it gives islands the
|
|
11
11
|
* server code they need (values, action references); `renderToStream` renders a page.
|
|
12
12
|
*/
|
|
13
|
+
const PROMISE_PREFIX = `${Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2)), (part) => part.toString(36)).join("")}.`;
|
|
14
|
+
let promiseCount = 0;
|
|
15
|
+
function isThenable(value) {
|
|
16
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
17
|
+
}
|
|
13
18
|
setIslandServerHook({
|
|
14
19
|
encode(props, held) {
|
|
20
|
+
const promises = [];
|
|
15
21
|
const addressed = addressValues(props, { encodeSpecial: (value) => {
|
|
16
22
|
if (value && typeof value === "object" && runtimeTagged.has(value)) return value;
|
|
23
|
+
if (isThenable(value)) {
|
|
24
|
+
const name = PROMISE_PREFIX + (promiseCount++).toString(36);
|
|
25
|
+
promises.push({
|
|
26
|
+
name,
|
|
27
|
+
promise: value
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
["$"]: "T",
|
|
31
|
+
["v"]: name
|
|
32
|
+
};
|
|
33
|
+
}
|
|
17
34
|
if (typeof value === "function") {
|
|
18
35
|
const reference = encodeActionReference(value);
|
|
19
36
|
if (reference) return reference;
|
|
@@ -26,7 +43,8 @@ setIslandServerHook({
|
|
|
26
43
|
values: selectValuesToSend(addressed, held).map(({ key, json }) => ({
|
|
27
44
|
key,
|
|
28
45
|
json
|
|
29
|
-
}))
|
|
46
|
+
})),
|
|
47
|
+
promises
|
|
30
48
|
};
|
|
31
49
|
},
|
|
32
50
|
escape: escapeScriptJson,
|
|
@@ -81,4 +99,4 @@ async function renderToStream(element, { request, formState, bootstrap = true, p
|
|
|
81
99
|
function isPersonalized(request) {
|
|
82
100
|
return parseHeldValues(request).size > 0;
|
|
83
101
|
}
|
|
84
|
-
export { IslandsProvider, PAYLOAD_ONLY_ATTRIBUTE, createIsland, getClientReference, isPersonalized, isolateIslands, renderServerTree, renderToStream, setRenderStateResolver, withinClientRoot };
|
|
102
|
+
export { IslandsProvider, Outlet, PAYLOAD_ONLY_ATTRIBUTE, createIsland, getClientReference, isPersonalized, isolateIslands, renderServerTree, renderToStream, setRenderStateResolver, withinClientRoot };
|
package/dist/react.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { f as IslandRenderState, g as isolateIslands, l as renderServerTree, m as createIsland, o as ServerTreeRenderOptions, p as IslandsProvider, t as Outlet, v as withinClientRoot } from "./_chunks/tree-B4RMU2UX.mjs";
|
|
2
2
|
import { n as IslandStrategy } from "./_chunks/schedule-Dw14dW0H.mjs";
|
|
3
|
-
export { type IslandRenderState, type IslandStrategy, IslandsProvider, type ServerTreeRenderOptions, createIsland, isolateIslands, renderServerTree, withinClientRoot };
|
|
3
|
+
export { type IslandRenderState, type IslandStrategy, IslandsProvider, Outlet, type ServerTreeRenderOptions, createIsland, isolateIslands, renderServerTree, withinClientRoot };
|
package/dist/react.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { s as IslandsProvider, t as Outlet, c as createIsland, d as isolateIslands, a as renderServerTree, h as withinClientRoot } from "./_chunks/tree-opIpaCke.mjs";
|
package/dist/server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { _ as hashValue, a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, g as escapeScriptJson, h as addressValues, i as encodeActionReference, l as SealError, m as VALUE_MIN_LENGTH, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys, v as parseHeldValues, y as selectValuesToSend } from "./_chunks/actions-
|
|
2
|
-
import { a as encode, r as decode } from "./_chunks/codec-
|
|
3
|
-
import {
|
|
1
|
+
import { _ as hashValue, a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, g as escapeScriptJson, h as addressValues, i as encodeActionReference, l as SealError, m as VALUE_MIN_LENGTH, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys, v as parseHeldValues, y as selectValuesToSend } from "./_chunks/actions-CtLpJPEB.mjs";
|
|
2
|
+
import { a as encode, r as decode } from "./_chunks/codec-C8to-RQW.mjs";
|
|
3
|
+
import { I as readPostedAction, L as stripRuntimeFields, N as attachmentField, n as ACTION_KEY_FIELD, o as BOUND_FIELD, r as ACTION_STALE_HEADER, s as CALL_FIELD, t as ACTION_HEADER } from "./_chunks/wire-QEQWBr5s.mjs";
|
|
4
4
|
import { n as setBuildDefaults, t as getBuildDefaults } from "./_chunks/build-COIcs7wm.mjs";
|
|
5
5
|
// src/server/hooks.ts
|
|
6
6
|
const HOOKS_KEY = Symbol.for("islx.server.hooks");
|
package/dist/vite.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as transformActions, r as hasModuleDirective, t as ActionCompileError } from "./_chunks/actions-Eutk6UzT.mjs";
|
|
2
|
-
import { t as ISLAND_STRATEGIES } from "./_chunks/schedule-
|
|
2
|
+
import { t as ISLAND_STRATEGIES } from "./_chunks/schedule-CjoybS3b.mjs";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { walk } from "estree-walker";
|
package/package.json
CHANGED