@farm.js/islx 0.0.3 → 0.0.5

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 CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.5
4
+
5
+ TanStack Start.
6
+
7
+ - `@farm.js/islx/tanstack-start`: `islxTanstackStart()` next to `tanstackStart()` runs a Start app
8
+ as islands, with configuration only. Routes, loaders and layouts render on the server; the
9
+ browser loads islx and the `"use client"` components a page uses (the modules under `src/`), not
10
+ the router, the route tree or loader data. Start's client entry becomes islx's and the router is
11
+ not dehydrated. Links move between routes without loading a document; a loader's promise passed
12
+ unawaited to a client component streams in; islands call Start server functions and islx server
13
+ actions. An app with its own `src/server.ts` uses `islandsHandler` from
14
+ `@farm.js/islx/tanstack-start/server`. Needs Node 22.12 or later (Start's own requirement).
15
+
16
+ ## 0.0.4
17
+
18
+ Promise props, and islands in a page that streams after it loaded.
19
+
20
+ - A server component can pass an unawaited promise to a client component, which reads it with
21
+ `use()`. The promise travels as `{"$":"T","v":name}`; when it settles, a Suspense boundary after
22
+ the island writes `<script type="f/promise" name>` with `{"v":value}` (the same codec and values
23
+ sent once) or `{"e":1}`. A rejection's reason stays on the server. In the browser the prop is a
24
+ thenable: settled already when its outcome is on the page, so hydration does not suspend, and
25
+ settled when it streams in otherwise.
26
+ - An island holding a Suspense boundary that is still streaming, after the document has loaded (a
27
+ framework router that streams navigations), hydrates once React reveals the boundary instead of
28
+ React rendering it again in the browser (error #419). `endStreamedContent(root)` from
29
+ `@farm.js/islx/react/client` marks boundaries a stream never completed as rendered in the
30
+ browser, so their islands hydrate on the next pass.
31
+
3
32
  ## 0.0.3
4
33
 
5
34
  Server layouts as trees.
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.3. React and Vite are supported; other renderers and framework
16
+ > Status: 0.0.5. React, Vite and TanStack Start 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
 
@@ -169,6 +169,41 @@ export function Chart(props: { points: Map<string, number> }) { … }
169
169
  `examples/react` is a runnable app; `pnpm test:e2e` runs it in Chrome, in development and from
170
170
  its production build.
171
171
 
172
+ ## TanStack Start
173
+
174
+ A Start app as islands, with configuration only:
175
+
176
+ ```ts
177
+ // vite.config.ts
178
+ import { defineConfig } from "vite";
179
+ import { tanstackStart } from "@tanstack/react-start/plugin/vite";
180
+ import react from "@vitejs/plugin-react";
181
+ import { islxTanstackStart } from "@farm.js/islx/tanstack-start";
182
+
183
+ export default defineConfig({ plugins: [tanstackStart(), react(), islxTanstackStart()] });
184
+ ```
185
+
186
+ Routes, loaders and layouts render on the server. The browser loads islx and the `"use client"`
187
+ components a page uses (the modules under `src/`), not the router, the route tree or loader
188
+ data: Start's client entry becomes islx's, `<Scripts />` still loads it, and the router is not
189
+ dehydrated. Links move between routes without loading a document. A loader's promise passed
190
+ unawaited to a client component streams in. Islands call Start server functions
191
+ (`createServerFn`) and islx server actions (`"use server"`).
192
+
193
+ - An app with its own `src/server.ts` renders with islx's handler:
194
+ `createServerEntry({ fetch: createStartHandler(islandsHandler) })`, with `islandsHandler` from
195
+ `@farm.js/islx/tanstack-start/server` (and `handleAction` from `@farm.js/islx/server` in front of
196
+ it for islx actions).
197
+ - Client components run without a router in the browser: they cannot use `useRouter`,
198
+ `useNavigate` or `<Link>` (render plain `<a>` links; the server renders `<Link>` in routes).
199
+ - A library's `"use client"` components render as server HTML.
200
+ - A click before the islands hydrate is not replayed yet (the `interaction` strategy hydrates on
201
+ it, but the click itself is dropped).
202
+ - Start sends a bot the whole page before any of it; people get it streamed.
203
+ - Start needs Node 22.12 or later.
204
+
205
+ `examples/tanstack-start` runs in the e2e suite, in development and from its production build.
206
+
172
207
  ## In a framework
173
208
 
174
209
  A framework that renders pages itself (Farm does) uses the pieces directly instead of
package/SPEC.md CHANGED
@@ -35,8 +35,9 @@ Anything that writes and reads it, in any language or framework, is a islx imple
35
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
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
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 props script's JSON escapes `<`, U+2028 and U+2029.
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,6 +59,8 @@ 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 | |
62
65
  | `L` | a layout tree's outlet, where the page renders | `n`: the outlet's name, `children` for the page |
63
66
  | `F` | a `FormData` action argument | its index among the call's attachments |
@@ -102,6 +105,13 @@ islands, and handles history, scroll and focus. Anything unusual loads the page
102
105
  site, `target`, `download`, `rel="external"`, modifier keys, a non-HTML answer, or a page whose
103
106
  browser entry differs, as after a deploy).
104
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
+
105
115
  A server that leaves layouts out renders each kept layout as an empty boundary, and the browser
106
116
  keeps its own. If the response does not fit what is on screen, the browser loads the full
107
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 VALUE_SCRIPT_TYPE, N as getTag, O as VALUES_HEADER, j as attachmentField, k as VALUE_KEY_PATTERN, o as BOUND_FIELD, s as CALL_FIELD, t as ACTION_HEADER } from "./wire-CXOodE06.mjs";
2
- import { a as encode, r as decode, u as setOwn } from "./codec-DqfVYKsx.mjs";
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,147 +1,6 @@
1
- import { M as formFieldName, O as VALUES_HEADER, d as FORM_REF_PREFIX, k as VALUE_KEY_PATTERN } from "./wire-CXOodE06.mjs";
2
- import { a as encode, c as needsObjectTag, o as encodeLeaf, r as decode, t as EncodeError } from "./codec-DqfVYKsx.mjs";
1
+ import { P as formFieldName, d as FORM_REF_PREFIX } from "./wire-QEQWBr5s.mjs";
3
2
  import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
4
- // src/server/values.ts
5
- /**
6
- * Values sent once: which parts of an island's props travel as values of their own, keyed by a
7
- * hash of their JSON, and which of those a response leaves out because the browser already holds
8
- * them. The browser side is `@farm.js/islx/client` (`getValueStore`).
9
- */
10
- /** Shorter values stay inline: a reference and its script cost about 60 bytes. */
11
- const VALUE_MIN_LENGTH = 512;
12
- /** A 64-bit hash of a value's JSON, as 14 base-36 characters. Not cryptographic. */
13
- function hashValue(json) {
14
- let h1 = 3735928559 ^ json.length;
15
- let h2 = 1103547991 ^ json.length;
16
- for (let index = 0; index < json.length; index++) {
17
- const code = json.charCodeAt(index);
18
- h1 = Math.imul(h1 ^ code, 2654435761);
19
- h2 = Math.imul(h2 ^ code, 1597334677);
20
- }
21
- h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
22
- h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
23
- return (h2 >>> 0).toString(36).padStart(7, "0") + (h1 >>> 0).toString(36).padStart(7, "0");
24
- }
25
- /**
26
- * Serializes props with every value whose JSON reaches `minLength` replaced by a reference to it.
27
- * Types JSON cannot hold are tagged first (the codec), so a `Map` or a `Date` is addressed like
28
- * any other value. Inner values are addressed first, so a large value that differs between pages
29
- * in one place still shares its unchanged parts.
30
- */
31
- function addressValues(props, { minLength = 512, encodeSpecial, encoded = false } = {}) {
32
- const values = /* @__PURE__ */ new Map();
33
- const reference = (json, children, parent) => {
34
- const key = hashValue(json);
35
- if (!values.has(key)) values.set(key, {
36
- key,
37
- json,
38
- children
39
- });
40
- parent.push(key);
41
- return `{"$":"r","v":"${key}"}`;
42
- };
43
- const seen = /* @__PURE__ */ new Set();
44
- const encode = (value, parent) => {
45
- const special = encoded ? void 0 : encodeSpecial?.(value);
46
- if (special !== void 0) {
47
- const json = JSON.stringify(special);
48
- return [json, json.length];
49
- }
50
- const leaf = encoded ? void 0 : encodeLeaf(value);
51
- if (leaf) {
52
- const json = JSON.stringify(leaf);
53
- return [json, json.length];
54
- }
55
- if (typeof value === "function") throw new TypeError("functions are not serializable");
56
- if (typeof value === "symbol") throw new TypeError("symbol values are not serializable");
57
- if (value === null || typeof value !== "object") {
58
- const json = JSON.stringify(value);
59
- return [typeof value === "string" && json.length >= minLength ? reference(json, [], parent) : json, json.length];
60
- }
61
- if (seen.has(value)) throw new TypeError("circular values are not serializable");
62
- seen.add(value);
63
- try {
64
- if (!encoded && (value instanceof Map || value instanceof Set)) {
65
- const entries = value instanceof Map ? Array.from(value, ([key, entry]) => [key, entry]) : [...value];
66
- const [json, length] = encode(entries, parent);
67
- const prefix = `{"$":"${value instanceof Map ? "M" : "S"}","v":`;
68
- return [`${prefix}${json}}`, length + prefix.length + 1];
69
- }
70
- const prototype = Object.getPrototypeOf(value);
71
- if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) throw new TypeError("class instances are not serializable");
72
- const own = [];
73
- let length = 2;
74
- const parts = [];
75
- if (Array.isArray(value)) for (const entry of value) {
76
- const [json, entryLength] = encode(entry, own);
77
- parts.push(json);
78
- length += entryLength + 1;
79
- }
80
- else for (const [key, entry] of Object.entries(value)) {
81
- const [json, entryLength] = encode(entry, own);
82
- const name = JSON.stringify(key);
83
- parts.push(`${name}:${json}`);
84
- length += name.length + entryLength + 2;
85
- }
86
- let json = Array.isArray(value) ? `[${parts.join(",")}]` : `{${parts.join(",")}}`;
87
- if (!encoded && !Array.isArray(value) && needsObjectTag(value)) {
88
- json = `{"$":"o","v":${json}}`;
89
- length += 14;
90
- }
91
- const wrapper = own.length === 1 && json.length < minLength / 4;
92
- if (length >= minLength && !wrapper) return [reference(json, own, parent), length];
93
- parent.push(...own);
94
- return [json, length];
95
- } finally {
96
- seen.delete(value);
97
- }
98
- };
99
- const roots = [];
100
- return {
101
- json: `{${Object.entries(props).map(([key, entry]) => `${JSON.stringify(key)}:${encode(entry, roots)[0]}`).join(",")}}`,
102
- values,
103
- roots
104
- };
105
- }
106
- /**
107
- * The values a response sends for `addressed`, parents first. A value the browser holds is left
108
- * out with everything inside it: the browser kept those when it read the value.
109
- */
110
- function selectValuesToSend(addressed, held) {
111
- const send = [];
112
- const seen = /* @__PURE__ */ new Set();
113
- const visit = (key) => {
114
- if (seen.has(key)) return;
115
- seen.add(key);
116
- if (held?.has(key)) return;
117
- const value = addressed.values.get(key);
118
- send.push(value);
119
- value.children.forEach(visit);
120
- };
121
- addressed.roots.forEach(visit);
122
- return send;
123
- }
124
- /**
125
- * The value keys a request names (`F-Values`): well-formed keys only, at most `MAX_HELD_VALUES`.
126
- *
127
- * A response that leaves values out is one browser's. Integrations must keep it out of shared
128
- * caches (CDN, ISR, PPR shells), and add `Vary: F-Values` to cacheable responses.
129
- */
130
- function parseHeldValues(input) {
131
- const header = typeof input === "string" || input == null ? input : (input instanceof Request ? input.headers : input).get(VALUES_HEADER);
132
- const held = /* @__PURE__ */ new Set();
133
- if (!header) return held;
134
- for (const key of header.split(",", 256)) {
135
- const trimmed = key.trim();
136
- if (VALUE_KEY_PATTERN.test(trimmed)) held.add(trimmed);
137
- if (held.size === 128) break;
138
- }
139
- return held;
140
- }
141
- /** A value script's JSON, safe inside a `<script>` element. */
142
- function escapeScriptJson(json) {
143
- return json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
144
- }
3
+ import { a as encode, r as decode, t as EncodeError } from "./codec-C8to-RQW.mjs";
145
4
  // src/server/seal.ts
146
5
  /**
147
6
  * Sealing an action's captured values. An inline `"use server"` function that reads a variable of
@@ -612,4 +471,4 @@ async function resolveAction(id, token) {
612
471
  run: async (args) => runEntry(entry, values, args)
613
472
  };
614
473
  }
615
- export { hashValue as _, getActionReference as a, resolveAction as c, generateActionKey as d, sealBound as f, escapeScriptJson as g, addressValues as h, encodeActionReference as i, SealError as l, VALUE_MIN_LENGTH as m, configureActionKeys as n, registerInlineAction as o, unsealBound as p, decodeActionReference as r, registerServerReference as s, bindInlineAction as t, createActionKeys as u, parseHeldValues as v, selectValuesToSend as y };
474
+ export { getActionReference as a, resolveAction as c, generateActionKey as d, sealBound as f, encodeActionReference as i, SealError as l, configureActionKeys as n, registerInlineAction as o, unsealBound as p, decodeActionReference as r, registerServerReference as s, bindInlineAction as t, createActionKeys as u };
@@ -1,4 +1,4 @@
1
- import { N as getTag } from "./wire-CXOodE06.mjs";
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": return NOT_A_VALUE;
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;
@@ -0,0 +1,262 @@
1
+ import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
2
+ import { M as VALUE_SCRIPT_TYPE, h as ISLAND_ELEMENT, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "./wire-QEQWBr5s.mjs";
3
+ import * as React from "react";
4
+ import * as ReactDOM from "react-dom";
5
+ // src/react/island.ts
6
+ /**
7
+ * A client component, as the server renders it: its HTML inside `<f-island>`, and its props in
8
+ * a script after it. Shared by the server and the browser: inside a hydrating island, a nested
9
+ * client component is part of that island's React tree, not an island of its own.
10
+ *
11
+ * The compiler wraps every export of a `"use client"` module with `createIsland` in the server
12
+ * build. The browser build keeps the module as it is.
13
+ */
14
+ const CLIENT_REFERENCE = Symbol.for("islx.react.client-reference");
15
+ /** The module export an island component renders, or undefined for any other type. */
16
+ function getClientReference(type) {
17
+ if (!type || typeof type !== "function" && typeof type !== "object") return void 0;
18
+ return type[CLIENT_REFERENCE];
19
+ }
20
+ /**
21
+ * Makes `element`'s client components part of the React root around it (a layout that hydrates
22
+ * as one root), instead of islands of their own.
23
+ */
24
+ function withinClientRoot(element) {
25
+ return React.createElement(getContexts().graph.Provider, { value: true }, element);
26
+ }
27
+ /**
28
+ * Starts islands again below a `withinClientRoot`: the page a hydrating layout keeps as server
29
+ * HTML renders its client components as islands of their own.
30
+ */
31
+ function isolateIslands(element) {
32
+ return React.createElement(getContexts().graph.Provider, { value: false }, element);
33
+ }
34
+ const RENDER_STATE_RESOLVER = Symbol.for("islx.react.render-state");
35
+ /**
36
+ * For a framework that knows the request being rendered without React context (a request store):
37
+ * returns the render's settings when no `IslandsProvider` or `renderToStream` gave them. Keep the
38
+ * same `sent` set for the whole response.
39
+ */
40
+ function setRenderStateResolver(resolve) {
41
+ globalThis[RENDER_STATE_RESOLVER] = resolve;
42
+ }
43
+ function resolveRenderState() {
44
+ const resolve = globalThis[RENDER_STATE_RESOLVER];
45
+ return resolve ? resolve() : null;
46
+ }
47
+ /**
48
+ * Gives islands a render's settings when the framework renders the page itself (not through
49
+ * `renderToStream`): which values the browser holds, and which this render already wrote.
50
+ */
51
+ function IslandsProvider({ held = null, children }) {
52
+ const state = React.useMemo(() => ({
53
+ held: held && held.size > 0 ? held : null,
54
+ sent: /* @__PURE__ */ new Set()
55
+ }), [held]);
56
+ return React.createElement(getContexts().render.Provider, { value: state }, children);
57
+ }
58
+ const SERVER_HOOK = Symbol.for("islx.react.server");
59
+ /** @internal */
60
+ function getIslandServerHook() {
61
+ return globalThis[SERVER_HOOK];
62
+ }
63
+ /** @internal */
64
+ function setIslandServerHook(hook) {
65
+ globalThis[SERVER_HOOK] = hook;
66
+ }
67
+ /** Tagged objects the runtime made (slots), as opposed to app data with a "$" key. */
68
+ const runtimeTagged = /* @__PURE__ */ new WeakSet();
69
+ const CONTEXTS = Symbol.for("islx.react.contexts");
70
+ /** @internal */
71
+ function getContexts() {
72
+ const scope = globalThis;
73
+ const all = scope[CONTEXTS] ??= /* @__PURE__ */ new WeakMap();
74
+ let contexts = all.get(React.createElement);
75
+ if (!contexts) {
76
+ contexts = {
77
+ graph: React.createContext(false),
78
+ render: React.createContext(null)
79
+ };
80
+ all.set(React.createElement, contexts);
81
+ }
82
+ return contexts;
83
+ }
84
+ function isElement(value) {
85
+ const type = value?.$$typeof;
86
+ return type === Symbol.for("react.transitional.element") || type === Symbol.for("react.element");
87
+ }
88
+ const slotStyle = { display: "contents" };
89
+ /**
90
+ * Splits props into what renders on the server and what travels: server output passed to an
91
+ * island (`children`, or any element prop) renders once inside `<f-slot>`, and travels as a
92
+ * reference to it, so the browser reads its HTML from the page instead of receiving it twice.
93
+ */
94
+ function splitSlots(value, prefix, counter) {
95
+ if (isElement(value)) {
96
+ const name = `${prefix}${counter.next++}`;
97
+ const reference = {
98
+ ["$"]: "s",
99
+ ["v"]: name
100
+ };
101
+ runtimeTagged.add(reference);
102
+ return {
103
+ render: React.createElement(SLOT_ELEMENT, {
104
+ key: value.key ?? name,
105
+ name,
106
+ style: slotStyle
107
+ }, value),
108
+ wire: reference
109
+ };
110
+ }
111
+ if (Array.isArray(value)) {
112
+ const parts = value.map((entry) => splitSlots(entry, prefix, counter));
113
+ return {
114
+ render: parts.map((part) => part.render),
115
+ wire: parts.map((part) => part.wire)
116
+ };
117
+ }
118
+ if (value && typeof value === "object") {
119
+ const prototype = Object.getPrototypeOf(value);
120
+ if (prototype !== Object.prototype && prototype !== null) return {
121
+ render: value,
122
+ wire: value
123
+ };
124
+ const render = {};
125
+ const wire = {};
126
+ for (const [key, entry] of Object.entries(value)) {
127
+ const part = splitSlots(entry, prefix, counter);
128
+ render[key] = part.render;
129
+ wire[key] = part.wire;
130
+ }
131
+ return {
132
+ render,
133
+ wire
134
+ };
135
+ }
136
+ return {
137
+ render: value,
138
+ wire: value
139
+ };
140
+ }
141
+ const warned = /* @__PURE__ */ new Set();
142
+ const outcomes = /* @__PURE__ */ new WeakMap();
143
+ function settle(promise) {
144
+ let outcome = outcomes.get(promise);
145
+ if (!outcome) {
146
+ outcome = Promise.resolve(promise).then((value) => ({
147
+ ok: true,
148
+ value
149
+ }), (reason) => ({
150
+ ok: false,
151
+ reason
152
+ }));
153
+ outcomes.set(promise, outcome);
154
+ }
155
+ return outcome;
156
+ }
157
+ const REJECTED = "{\"e\":1}";
158
+ /** Each promise an island received, in a boundary of its own: its outcome once it settles. */
159
+ function promiseBoundaries(promises, label) {
160
+ return promises.map(({ name, promise }) => React.createElement(React.Suspense, {
161
+ key: name,
162
+ fallback: null
163
+ }, React.createElement(IslandPromise, {
164
+ name,
165
+ promise,
166
+ label
167
+ })));
168
+ }
169
+ /**
170
+ * A promise's outcome, written when it settles. Its values are sent whether or not the page wrote
171
+ * them already: this script may land before an earlier island's, wherever React puts it.
172
+ */
173
+ function IslandPromise({ name, promise, label }) {
174
+ const outcome = React.use(settle(promise));
175
+ const state = React.useContext(getContexts().render) ?? resolveRenderState();
176
+ const hook = getIslandServerHook();
177
+ let encoded = {
178
+ json: REJECTED,
179
+ values: [],
180
+ promises: []
181
+ };
182
+ if (outcome.ok) try {
183
+ encoded = hook.encode({ v: outcome.value }, state?.held ?? null);
184
+ } catch (error) {
185
+ console.warn(`[islx] A promise passed to ${label} resolved to a value that cannot be sent to the browser: it rejects there.`, error);
186
+ }
187
+ else console.error(`[islx] A promise passed to ${label} rejected.`, outcome.reason);
188
+ return React.createElement(React.Fragment, null, ...encoded.values.map((value) => React.createElement("script", {
189
+ key: value.key,
190
+ type: VALUE_SCRIPT_TYPE,
191
+ name: value.key,
192
+ dangerouslySetInnerHTML: { __html: hook.escape(value.json) }
193
+ })), React.createElement("script", {
194
+ key: "outcome",
195
+ type: PROMISE_SCRIPT_TYPE,
196
+ name,
197
+ dangerouslySetInnerHTML: { __html: hook.escape(encoded.json) }
198
+ }), ...promiseBoundaries(encoded.promises, label));
199
+ }
200
+ function isThenable(value) {
201
+ return !!value && typeof value.then === "function";
202
+ }
203
+ /**
204
+ * Wraps a client component so the server renders it as an island. `src` is the module the browser
205
+ * imports, `exportName` the component's export there.
206
+ */
207
+ function createIsland(Component, src, exportName, strategy = "load") {
208
+ function Island(props) {
209
+ const { graph, render } = getContexts();
210
+ const insideIsland = React.useContext(graph);
211
+ const state = React.useContext(render) ?? resolveRenderState();
212
+ const id = React.useId();
213
+ if (insideIsland) return React.createElement(Component, props);
214
+ const hook = getIslandServerHook();
215
+ if (!hook) throw new Error("islx: an island rendered on a server that did not import @farm.js/islx/react/server");
216
+ let encoded;
217
+ let renderProps;
218
+ try {
219
+ const { render: rendered, wire } = splitSlots(props, `${id}s`, { next: 0 });
220
+ renderProps = rendered;
221
+ const skip = state ? /* @__PURE__ */ new Set([...state.held ?? [], ...state.sent]) : null;
222
+ encoded = hook.encode(wire, skip);
223
+ for (const value of encoded.values) state?.sent.add(value.key);
224
+ } catch (error) {
225
+ if (isThenable(error)) throw error;
226
+ const message = error instanceof Error ? error.message : String(error);
227
+ const key = `${src}#${exportName}:${message}`;
228
+ if (!warned.has(key)) {
229
+ warned.add(key);
230
+ console.warn(`[islx] ${exportName} (${src}) was rendered without hydration: its props cannot be sent to the browser. ${message}`);
231
+ }
232
+ return React.createElement(Component, props);
233
+ }
234
+ if (strategy === "load") for (const href of getBuildDefaults().modules?.[src] ?? []) ReactDOM.preloadModule?.(href, { as: "script" });
235
+ return React.createElement(React.Fragment, null, ...encoded.values.map((value) => React.createElement("script", {
236
+ key: value.key,
237
+ type: VALUE_SCRIPT_TYPE,
238
+ name: value.key,
239
+ dangerouslySetInnerHTML: { __html: hook.escape(value.json) }
240
+ })), React.createElement(ISLAND_ELEMENT, {
241
+ key: "island",
242
+ src,
243
+ export: exportName,
244
+ props: id,
245
+ strategy,
246
+ style: slotStyle
247
+ }, React.createElement(graph.Provider, { value: true }, React.createElement(Component, renderProps))), React.createElement("script", {
248
+ key: "props",
249
+ type: PROPS_SCRIPT_TYPE,
250
+ htmlFor: id,
251
+ dangerouslySetInnerHTML: { __html: hook.escape(encoded.json) }
252
+ }), ...promiseBoundaries(encoded.promises, `${exportName} (${src})`));
253
+ }
254
+ Island.displayName = `Island(${Component.displayName || Component.name || exportName})`;
255
+ Object.defineProperty(Island, CLIENT_REFERENCE, { value: {
256
+ src,
257
+ exportName,
258
+ Component
259
+ } });
260
+ return Island;
261
+ }
262
+ export { getIslandServerHook as a, setIslandServerHook as c, getContexts as i, setRenderStateResolver as l, createIsland as n, isolateIslands as o, getClientReference as r, runtimeTagged as s, IslandsProvider as t, withinClientRoot as u };
@@ -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-CXOodE06.mjs";
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.