@farm.js/islx 0.0.0-stage → 0.0.2

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +198 -2
  4. package/SPEC.md +220 -0
  5. package/dist/_chunks/actions-2sTj0A2T.mjs +306 -0
  6. package/dist/_chunks/actions-B8dJ8f7L.mjs +615 -0
  7. package/dist/_chunks/actions-BCtIGvrP.d.mts +58 -0
  8. package/dist/_chunks/actions-Eutk6UzT.mjs +326 -0
  9. package/dist/_chunks/build-COIcs7wm.mjs +9 -0
  10. package/dist/_chunks/codec-CK8kZp4u.d.mts +64 -0
  11. package/dist/_chunks/codec-DqfVYKsx.mjs +279 -0
  12. package/dist/_chunks/handle-C0N9ZyNV.d.mts +35 -0
  13. package/dist/_chunks/hooks-3VZV8f_d.d.mts +30 -0
  14. package/dist/_chunks/hooks-De0gNpOn.d.mts +64 -0
  15. package/dist/_chunks/prehydration-B1GLTLdk.mjs +36 -0
  16. package/dist/_chunks/schedule-BFoGKRrU.mjs +216 -0
  17. package/dist/_chunks/schedule-Dw14dW0H.d.mts +17 -0
  18. package/dist/_chunks/tree-BDB5VEao.mjs +777 -0
  19. package/dist/_chunks/tree-fID6sz25.d.mts +102 -0
  20. package/dist/_chunks/value-store-Dm-7xJ6W.d.mts +19 -0
  21. package/dist/_chunks/wire-C3Rpbwl6.d.mts +98 -0
  22. package/dist/_chunks/wire-CXOodE06.mjs +121 -0
  23. package/dist/client.d.mts +58 -0
  24. package/dist/client.mjs +197 -0
  25. package/dist/compiler.d.mts +2 -0
  26. package/dist/compiler.mjs +1 -0
  27. package/dist/index.d.mts +3 -0
  28. package/dist/index.mjs +3 -0
  29. package/dist/react/client.d.mts +49 -0
  30. package/dist/react/client.mjs +456 -0
  31. package/dist/react/server.d.mts +51 -0
  32. package/dist/react/server.mjs +84 -0
  33. package/dist/react.d.mts +3 -0
  34. package/dist/react.mjs +1 -0
  35. package/dist/server.d.mts +200 -0
  36. package/dist/server.mjs +378 -0
  37. package/dist/vite.d.mts +57 -0
  38. package/dist/vite.mjs +497 -0
  39. package/package.json +121 -4
@@ -0,0 +1,49 @@
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-C3Rpbwl6.mjs";
3
+ import { t as ValueStore } from "../_chunks/value-store-Dm-7xJ6W.mjs";
4
+ import { f as isolateIslands, i as createServerTreeElement, m as withinClientRoot, n as ServerTreeElementOptions, o as updateServerTrees, t as PAYLOAD_ONLY_ATTRIBUTE } from "../_chunks/tree-fID6sz25.mjs";
5
+ import * as React from "react";
6
+
7
+ // src/react/hydrate.d.ts
8
+
9
+ interface HydrationOptions {
10
+ /** Imports an island's module: `(src) => import(src)` with the bundler's URL. */
11
+ load(src: string): Promise<Record<string, unknown>>;
12
+ /** Wraps each island's element, as for providers every island needs. */
13
+ wrap?(element: React.ReactElement): React.ReactNode;
14
+ /** Reports an island that failed. Default `console.error`. */
15
+ report?(message: string, error?: unknown): void;
16
+ /** The value store. Default: the page's. */
17
+ values?: ValueStore;
18
+ /**
19
+ * When each island hydrates: `scheduleHydration` (its strategy), or the framework's own. Required,
20
+ * so a framework with its own scheduler ships none of islx's.
21
+ */
22
+ schedule: typeof scheduleHydration;
23
+ }
24
+ /** The hydration runtime for one page. */
25
+ declare function createHydrationRuntime(options: HydrationOptions): {
26
+ hydrate: (scope?: ParentNode, signal?: AbortSignal) => Promise<void>;
27
+ carryOver: (current: ParentNode, next: ParentNode) => number;
28
+ update: (src: string, module: Record<string, unknown>) => number;
29
+ dispose: (scope: Node) => void;
30
+ rootCount: () => number;
31
+ };
32
+ type HydrationRuntime = ReturnType<typeof createHydrationRuntime>;
33
+ /**
34
+ * A page a hydrating layout keeps as server HTML (and a server tree's `<f-html>` part). React owns
35
+ * only the wrapper, so the islands inside start after React commits that HTML and stop when it is
36
+ * replaced or the layout unmounts. The islands are captured when they start, because React has
37
+ * already swapped the HTML by the time the cleanup runs.
38
+ */
39
+ declare function createServerHtml(runtime: Pick<HydrationRuntime, "hydrate" | "dispose">): React.ComponentType<Record<string, unknown> & {
40
+ as?: string;
41
+ dangerouslySetInnerHTML: {
42
+ __html: string;
43
+ };
44
+ }>;
45
+ /** Fetches the current page and puts its new content in place, keeping island state. */
46
+ declare function refreshDocument(runtime: HydrationRuntime): Promise<void>;
47
+ /** Puts another page's body in place of this one's, and hydrates its islands. */
48
+ 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 };
@@ -0,0 +1,456 @@
1
+ import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-2sTj0A2T.mjs";
2
+ import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-DqfVYKsx.mjs";
3
+ import { N as getTag, S as SLOT_ELEMENT, f as FORM_STATE_SCRIPT_TYPE, h as ISLAND_ELEMENT, m as HYDRATED_ATTRIBUTE, y as PROPS_SCRIPT_TYPE } from "../_chunks/wire-CXOodE06.mjs";
4
+ import { c as getContexts, i as updateServerTrees, l as isolateIslands, n as createServerTreeElement, p as withinClientRoot, t as PAYLOAD_ONLY_ATTRIBUTE } from "../_chunks/tree-BDB5VEao.mjs";
5
+ import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-BFoGKRrU.mjs";
6
+ import * as React from "react";
7
+ import { hydrateRoot } from "react-dom/client";
8
+ // src/react/hydrate.ts
9
+ /**
10
+ * @farm.js/islx/react/client: hydrating a page's islands. Each island is its own React root,
11
+ * started when its strategy says. A root that fails restores the island's server HTML, so a broken
12
+ * component never blanks the page.
13
+ */
14
+ const ISLAND_SELECTOR = `${ISLAND_ELEMENT}[src]`;
15
+ function isEarlyUpdate(error) {
16
+ return error instanceof Error && (error.message.startsWith("This root received an early update") || error.message.startsWith("Minified React error #424;"));
17
+ }
18
+ function findPropsScript(container, id) {
19
+ const sibling = container.nextElementSibling;
20
+ if (sibling instanceof HTMLScriptElement && sibling.type === "f/props" && sibling.getAttribute("for") === id) return sibling;
21
+ for (const script of container.ownerDocument.querySelectorAll(`script[type="${PROPS_SCRIPT_TYPE}"]`)) if (script.getAttribute("for") === id) return script;
22
+ return null;
23
+ }
24
+ /** React's form state after a `useActionState` form post, if the page carries one. */
25
+ function readFormState(document) {
26
+ const script = document.querySelector(`script[type="${FORM_STATE_SCRIPT_TYPE}"]`);
27
+ if (!script) return void 0;
28
+ try {
29
+ return decode(JSON.parse(script.textContent || "null"));
30
+ } catch {
31
+ return;
32
+ }
33
+ }
34
+ /**
35
+ * Revived values, by the object the value store resolved. Islands that share a value (one sent
36
+ * once for several islands) then share the revived object instead of walking it again. A value
37
+ * with a slot is not kept: a slot's HTML belongs to one island.
38
+ */
39
+ const revivedValues = /* @__PURE__ */ new WeakMap();
40
+ let slotsRevived = 0;
41
+ /** Turns travelled props back into values: slots keep their server HTML, actions become callable. */
42
+ function revive(value, container) {
43
+ if (value && typeof value === "object") {
44
+ const cached = revivedValues.get(value);
45
+ if (cached !== void 0) return cached;
46
+ const slotsBefore = slotsRevived;
47
+ const revived = reviveUncached(value, container);
48
+ if (slotsRevived === slotsBefore) revivedValues.set(value, revived);
49
+ return revived;
50
+ }
51
+ return value;
52
+ }
53
+ function reviveUncached(value, container) {
54
+ if (Array.isArray(value)) {
55
+ let changed = false;
56
+ const revived = value.map((entry) => {
57
+ const next = revive(entry, container);
58
+ if (next !== entry) changed = true;
59
+ return next;
60
+ });
61
+ return changed ? revived : value;
62
+ }
63
+ const object = value;
64
+ const tag = getTag(object);
65
+ if (tag === "a" && typeof object["v"] === "string") {
66
+ const token = object["b"];
67
+ return createServerReference(object["v"], typeof token === "string" ? token : void 0);
68
+ }
69
+ if (tag === "s" && typeof object["v"] === "string") {
70
+ slotsRevived++;
71
+ const name = object["v"];
72
+ let html = "";
73
+ for (const slot of container.querySelectorAll(SLOT_ELEMENT)) if (slot.getAttribute("name") === name) {
74
+ html = slot.innerHTML;
75
+ break;
76
+ }
77
+ return React.createElement(SLOT_ELEMENT, {
78
+ key: name,
79
+ name,
80
+ style: { display: "contents" },
81
+ suppressHydrationWarning: true,
82
+ dangerouslySetInnerHTML: { __html: html }
83
+ });
84
+ }
85
+ if (tag === "r") throw new Error(`islx value ${String(object["v"])} was not resolved`);
86
+ if (tag !== void 0) {
87
+ const decoded = decodeTagged(object, (child) => revive(child, container));
88
+ if (decoded !== NOT_A_VALUE) return decoded;
89
+ }
90
+ let changed = false;
91
+ const revived = {};
92
+ for (const [key, entry] of Object.entries(object)) {
93
+ const next = revive(entry, container);
94
+ setOwn(revived, key, next);
95
+ if (next !== entry) changed = true;
96
+ }
97
+ return changed ? revived : value;
98
+ }
99
+ /**
100
+ * React streams Suspense content that resolves late into a hidden `<div id="S:n">`, then moves it
101
+ * into place. Its islands hydrate once moved, not while staged.
102
+ */
103
+ const STREAM_STAGING = "div[hidden][id^=\"S:\"]";
104
+ /** The hydration runtime for one page. */
105
+ function createHydrationRuntime(options) {
106
+ const roots = /* @__PURE__ */ new Map();
107
+ const pending = /* @__PURE__ */ new Map();
108
+ const values = options.values ?? getValueStore();
109
+ const schedule = options.schedule;
110
+ const report = options.report ?? ((message, error) => console.error(`[islx] ${message}`, error));
111
+ let formState;
112
+ let formStateRead = false;
113
+ const fail = (container, src, exportName, serverHTML, error) => {
114
+ report(`Could not hydrate ${exportName} (${src}). Its server HTML was restored.`, error);
115
+ emit("islandError", {
116
+ element: container,
117
+ src,
118
+ export: exportName,
119
+ error
120
+ });
121
+ queueMicrotask(() => {
122
+ try {
123
+ roots.get(container)?.root.unmount();
124
+ } catch {}
125
+ roots.delete(container);
126
+ container.removeAttribute(HYDRATED_ATTRIBUTE);
127
+ container.innerHTML = serverHTML;
128
+ });
129
+ };
130
+ class Boundary extends React.Component {
131
+ state = { failed: false };
132
+ static getDerivedStateFromError() {
133
+ return { failed: true };
134
+ }
135
+ componentDidCatch(error) {
136
+ this.props.onError(error);
137
+ }
138
+ render() {
139
+ return this.state.failed ? null : this.props.children;
140
+ }
141
+ }
142
+ /** Calls `onCommit` once, when React first commits the island: it is interactive then. */
143
+ function Committed({ onCommit, children }) {
144
+ const called = React.useRef(false);
145
+ React.useLayoutEffect(() => {
146
+ if (called.current || !onCommit) return;
147
+ called.current = true;
148
+ onCommit();
149
+ }, [onCommit]);
150
+ return children;
151
+ }
152
+ const element = (Component, props, restore, onCommit) => {
153
+ const { graph } = getContexts();
154
+ const inner = React.createElement(Component, props);
155
+ return React.createElement(graph.Provider, { value: true }, React.createElement(Boundary, { onError: restore }, React.createElement(Committed, {
156
+ onCommit,
157
+ children: options.wrap ? options.wrap(inner) : inner
158
+ })));
159
+ };
160
+ /** An island's props, with values, slots and actions revived. */
161
+ function readProps(container, id) {
162
+ const script = findPropsScript(container, id);
163
+ if (!script) throw new Error(`props ${id} were not found`);
164
+ const text = script.textContent || "{}";
165
+ let parsed = JSON.parse(text);
166
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("props must be an object");
167
+ if (!text.includes(`"\$"`)) return parsed;
168
+ if (text.includes(`"\$":"r"`)) {
169
+ values.ingest(container.ownerDocument);
170
+ parsed = values.resolve(parsed);
171
+ }
172
+ return revive(parsed, container);
173
+ }
174
+ const topLevel = (scope) => Array.from(scope.querySelectorAll(ISLAND_SELECTOR)).filter((container) => !container.parentElement?.closest(ISLAND_SELECTOR));
175
+ const keyOf = (container) => `${container.getAttribute("src")}#${container.getAttribute("export") || "default"}`;
176
+ /** Hydrates the islands under `scope` that are not hydrated or scheduled yet. */
177
+ async function hydrate(scope = document, signal) {
178
+ if (!formStateRead) {
179
+ formStateRead = true;
180
+ formState = readFormState(scope instanceof Document ? scope : scope.ownerDocument ?? document);
181
+ }
182
+ if (scope instanceof Document) watchStream(scope);
183
+ const candidates = Array.from(scope.querySelectorAll(ISLAND_SELECTOR));
184
+ 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));
186
+ await Promise.all(islands.map(async (container) => {
187
+ const src = container.getAttribute("src");
188
+ const id = container.getAttribute("props");
189
+ const exportName = container.getAttribute("export") || "default";
190
+ const strategy = container.getAttribute("strategy") ?? "load";
191
+ if (!src || !id) {
192
+ report("An island is missing its src or props id.");
193
+ return;
194
+ }
195
+ const controller = new AbortController();
196
+ pending.set(container, controller);
197
+ const release = () => {
198
+ if (pending.get(container) === controller) pending.delete(container);
199
+ };
200
+ controller.signal.addEventListener("abort", release, { once: true });
201
+ const abort = () => controller.abort();
202
+ if (signal?.aborted) abort();
203
+ else signal?.addEventListener("abort", abort, { once: true });
204
+ const cleanup = () => {
205
+ signal?.removeEventListener("abort", abort);
206
+ release();
207
+ };
208
+ let scheduled;
209
+ try {
210
+ const scheduledAt = performance.now();
211
+ scheduled = schedule({
212
+ container,
213
+ strategy,
214
+ signal: controller.signal,
215
+ hydrate: async () => {
216
+ if (controller.signal.aborted || !container.isConnected) return;
217
+ const serverHTML = container.innerHTML;
218
+ try {
219
+ const module = await options.load(src);
220
+ if (controller.signal.aborted || !container.isConnected) return;
221
+ const Component = module[exportName];
222
+ if (typeof Component !== "function" && (typeof Component !== "object" || Component === null)) throw new Error(`${src} has no export ${exportName}`);
223
+ const props = readProps(container, id);
224
+ let failure;
225
+ const restore = (error) => {
226
+ if (failure !== void 0) return;
227
+ failure = error;
228
+ fail(container, src, exportName, serverHTML, error);
229
+ controller.abort();
230
+ };
231
+ let disposing = false;
232
+ const onCommit = () => emit("hydrate", {
233
+ element: container,
234
+ src,
235
+ export: exportName,
236
+ strategy,
237
+ duration: performance.now() - scheduledAt
238
+ });
239
+ const root = hydrateRoot(container, element(Component, props, restore, onCommit), {
240
+ formState,
241
+ onUncaughtError: restore,
242
+ onRecoverableError(error) {
243
+ if (disposing && isEarlyUpdate(error)) return;
244
+ if (typeof globalThis.reportError === "function") globalThis.reportError(error);
245
+ else console.error(error);
246
+ }
247
+ });
248
+ roots.set(container, {
249
+ root,
250
+ Component,
251
+ src,
252
+ exportName,
253
+ props,
254
+ serverHTML,
255
+ restore,
256
+ dispose() {
257
+ disposing = true;
258
+ try {
259
+ root.unmount();
260
+ } finally {
261
+ disposing = false;
262
+ }
263
+ }
264
+ });
265
+ } catch (error) {
266
+ if (controller.signal.aborted) return;
267
+ fail(container, src, exportName, serverHTML, error);
268
+ controller.abort();
269
+ }
270
+ }
271
+ });
272
+ } catch (error) {
273
+ fail(container, src, exportName, container.innerHTML, error);
274
+ controller.abort();
275
+ cleanup();
276
+ return;
277
+ }
278
+ const tracked = Promise.resolve(scheduled).catch((error) => {
279
+ if (!controller.signal.aborted) {
280
+ fail(container, src, exportName, container.innerHTML, error);
281
+ controller.abort();
282
+ }
283
+ }).finally(cleanup);
284
+ if (strategy === "load") await tracked;
285
+ }));
286
+ }
287
+ /**
288
+ * Keeps the live islands of `current` when the same page renders again (after an action, or
289
+ * a refresh): each island of `next` that renders the same component at the same position takes
290
+ * over the live root with its new props, so its state survives. Call it before replacing
291
+ * `current`; islands it does not carry over hydrate from `next` as usual.
292
+ */
293
+ function carryOver(current, next) {
294
+ const live = /* @__PURE__ */ new Map();
295
+ for (const container of topLevel(current)) {
296
+ if (!roots.has(container)) continue;
297
+ const key = keyOf(container);
298
+ live.set(key, [...live.get(key) ?? [], container]);
299
+ }
300
+ let carried = 0;
301
+ for (const container of topLevel(next)) {
302
+ const previous = live.get(keyOf(container))?.shift();
303
+ const record = previous && roots.get(previous);
304
+ const id = container.getAttribute("props");
305
+ if (!previous || !record || !id) continue;
306
+ try {
307
+ const props = readProps(container, id);
308
+ record.root.render(element(record.Component, props, record.restore));
309
+ record.props = props;
310
+ container.replaceWith(previous);
311
+ carried++;
312
+ } catch (error) {
313
+ report(`Could not keep ${keyOf(container)} across the refresh.`, error);
314
+ }
315
+ }
316
+ return carried;
317
+ }
318
+ /** Re-renders the islands and server trees of a module that changed (hot module replacement). */
319
+ function update(src, module) {
320
+ let updated = updateServerTrees(src, module);
321
+ for (const [container, record] of roots) {
322
+ if (record.src !== src) continue;
323
+ const Component = module[record.exportName];
324
+ if (typeof Component !== "function" && (typeof Component !== "object" || Component === null)) {
325
+ fail(container, record.src, record.exportName, record.serverHTML, /* @__PURE__ */ new Error("the updated module lost the export"));
326
+ continue;
327
+ }
328
+ record.Component = Component;
329
+ record.root.render(element(record.Component, record.props, record.restore));
330
+ updated++;
331
+ }
332
+ return updated;
333
+ }
334
+ /** Unmounts the islands inside `scope`, and stops scheduled ones. */
335
+ /**
336
+ * A page still streaming: islands in Suspense content that arrives later hydrate as React puts it
337
+ * in place, and islands React removes (a replaced fallback) are released. Ends once the document
338
+ * is parsed and React has revealed every staged boundary (it may batch reveals past parsing).
339
+ */
340
+ let watching = false;
341
+ function watchStream(document) {
342
+ if (watching || document.readyState !== "loading" || typeof MutationObserver !== "function") return;
343
+ watching = true;
344
+ const removed = /* @__PURE__ */ new Set();
345
+ let queued = false;
346
+ const run = () => {
347
+ queued = false;
348
+ for (const node of removed) if (!node.isConnected) dispose(node);
349
+ removed.clear();
350
+ if (document.readyState !== "loading" && !document.querySelector(STREAM_STAGING)) {
351
+ observer.disconnect();
352
+ watching = false;
353
+ }
354
+ hydrate(document).catch((error) => report("Hydrating streamed islands failed.", error));
355
+ };
356
+ const observer = new MutationObserver((records) => {
357
+ for (const record of records) for (const node of record.removedNodes) removed.add(node);
358
+ if (queued) return;
359
+ queued = true;
360
+ queueMicrotask(run);
361
+ });
362
+ observer.observe(document, {
363
+ childList: true,
364
+ subtree: true
365
+ });
366
+ document.addEventListener("DOMContentLoaded", run, { once: true });
367
+ }
368
+ function dispose(scope) {
369
+ for (const [container, controller] of pending) if (container === scope || scope.contains(container)) {
370
+ controller.abort();
371
+ pending.delete(container);
372
+ }
373
+ for (const [container, record] of roots) if (container === scope || scope.contains(container)) {
374
+ try {
375
+ record.dispose();
376
+ } catch {}
377
+ roots.delete(container);
378
+ container.removeAttribute(HYDRATED_ATTRIBUTE);
379
+ }
380
+ }
381
+ return {
382
+ hydrate,
383
+ carryOver,
384
+ update,
385
+ dispose,
386
+ rootCount: () => roots.size
387
+ };
388
+ }
389
+ /**
390
+ * A page a hydrating layout keeps as server HTML (and a server tree's `<f-html>` part). React owns
391
+ * only the wrapper, so the islands inside start after React commits that HTML and stop when it is
392
+ * replaced or the layout unmounts. The islands are captured when they start, because React has
393
+ * already swapped the HTML by the time the cleanup runs.
394
+ */
395
+ function createServerHtml(runtime) {
396
+ return function ServerHtml({ as = "div", ...props }) {
397
+ const ref = React.useRef(null);
398
+ const html = props.dangerouslySetInnerHTML.__html;
399
+ React.useLayoutEffect(() => {
400
+ const page = ref.current;
401
+ if (!page) return;
402
+ const islands = Array.from(page.querySelectorAll(ISLAND_SELECTOR));
403
+ if (islands.length === 0) return;
404
+ const controller = new AbortController();
405
+ runtime.hydrate(page, controller.signal).catch((error) => {
406
+ console.error("[islx] Could not hydrate the islands in this part of the page:", error);
407
+ });
408
+ return () => {
409
+ controller.abort();
410
+ queueMicrotask(() => {
411
+ for (const island of islands) runtime.dispose(island);
412
+ });
413
+ };
414
+ }, [html]);
415
+ return React.createElement(as, {
416
+ ...props,
417
+ ref
418
+ });
419
+ };
420
+ }
421
+ // src/react/document.ts
422
+ /**
423
+ * Putting a fetched page in place without a router. After an action, the current page renders
424
+ * again and its live islands keep their state; after a navigation, the next page's islands
425
+ * start fresh, as a page change does. The runtime, its loaded code and its values stay in both.
426
+ * Frameworks with a router do this through it instead.
427
+ */
428
+ /** Fetches the current page and puts its new content in place, keeping island state. */
429
+ async function refreshDocument(runtime) {
430
+ const response = await fetch(location.href, {
431
+ headers: {
432
+ accept: "text/html",
433
+ ...valueHeaders()
434
+ },
435
+ credentials: "same-origin"
436
+ });
437
+ if (!response.ok || !response.headers.get("content-type")?.includes("text/html")) {
438
+ location.reload();
439
+ return;
440
+ }
441
+ const next = new DOMParser().parseFromString(await response.text(), "text/html");
442
+ if (next.title) document.title = next.title;
443
+ const body = document.importNode(next.body, true);
444
+ runtime.carryOver(document.body, body);
445
+ runtime.dispose(document.body);
446
+ document.body.replaceChildren(...Array.from(body.childNodes));
447
+ await runtime.hydrate(document);
448
+ }
449
+ /** Puts another page's body in place of this one's, and hydrates its islands. */
450
+ async function swapDocument(runtime, next) {
451
+ const body = document.importNode(next.body, true);
452
+ runtime.dispose(document.body);
453
+ document.body.replaceChildren(...Array.from(body.childNodes));
454
+ await runtime.hydrate(document);
455
+ }
456
+ export { HYDRATED_ATTRIBUTE, ISLAND_STRATEGIES, PAYLOAD_ONLY_ATTRIBUTE, createHydrationRuntime, createServerHtml, createServerTreeElement, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
@@ -0,0 +1,51 @@
1
+ import { r as ReactFormState } from "../_chunks/handle-C0N9ZyNV.mjs";
2
+ import { a as renderServerTree, c as IslandRenderState, d as getClientReference, f as isolateIslands, l as IslandsProvider, m as withinClientRoot, p as setRenderStateResolver, r as ServerTreeRenderOptions, s as ClientReference, t as PAYLOAD_ONLY_ATTRIBUTE, u as createIsland } from "../_chunks/tree-fID6sz25.mjs";
3
+ import * as React from "react";
4
+ import { RenderToReadableStreamOptions } from "react-dom/server";
5
+
6
+ // src/react/server.d.ts
7
+
8
+ interface RenderToStreamOptions extends Omit<RenderToReadableStreamOptions, "formState"> {
9
+ /**
10
+ * The request being answered: its `F-Values` header names the values the browser holds, and
11
+ * the page leaves them out. Leave it out for a response that may be shared (a CDN, a static
12
+ * page): every value is sent.
13
+ */
14
+ request?: Request;
15
+ /**
16
+ * The form state `handleAction` passed to `renderFormState`, after a form posted from a
17
+ * `useActionState` hook. The page shows the action's result, and so do its islands once they
18
+ * hydrate.
19
+ */
20
+ formState?: ReactFormState | null;
21
+ /**
22
+ * Adds the browser entry that hydrates islands (from the bundler plugin) to the page. Default
23
+ * true; a framework that adds its own scripts turns it off.
24
+ */
25
+ bootstrap?: boolean;
26
+ /**
27
+ * Adds the inline script that holds a click or a submit on an island before the browser entry
28
+ * has run, to replay it once the island hydrates. Default: with the browser entry.
29
+ */
30
+ prehydration?: boolean;
31
+ }
32
+ /**
33
+ * Renders a page whose client components are islands. A response rendered with a `request` whose
34
+ * browser holds values is that browser's alone: send it `private` and never share it.
35
+ *
36
+ * A navigation (`F-Render` on the request) gets the whole page in one piece: the browser puts it
37
+ * in place with DOM methods, which run no scripts, so the page cannot stream in with React's
38
+ * inline scripts. Every Suspense boundary has resolved before the first byte.
39
+ */
40
+ declare function renderToStream(element: React.ReactNode, {
41
+ request,
42
+ formState,
43
+ bootstrap,
44
+ prehydration,
45
+ ...options
46
+ }?: RenderToStreamOptions): Promise<ReadableStream<Uint8Array> & {
47
+ allReady: Promise<void>;
48
+ }>;
49
+ /** Whether a response rendered for `request` leaves out values its browser holds. */
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 };
@@ -0,0 +1,84 @@
1
+ import { a as encode } from "../_chunks/codec-DqfVYKsx.mjs";
2
+ import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-CXOodE06.mjs";
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-B8dJ8f7L.mjs";
5
+ import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
6
+ import { a as IslandsProvider, c as getContexts, d as setIslandServerHook, f as setRenderStateResolver, l as isolateIslands, o as createIsland, p as withinClientRoot, r as renderServerTree, s as getClientReference, t as PAYLOAD_ONLY_ATTRIBUTE, u as runtimeTagged } from "../_chunks/tree-BDB5VEao.mjs";
7
+ import * as React from "react";
8
+ // src/react/server.ts
9
+ /**
10
+ * @farm.js/islx/react/server: rendering a React page with islands. Importing it gives islands the
11
+ * server code they need (values, action references); `renderToStream` renders a page.
12
+ */
13
+ setIslandServerHook({
14
+ encode(props, held) {
15
+ const addressed = addressValues(props, { encodeSpecial: (value) => {
16
+ if (value && typeof value === "object" && runtimeTagged.has(value)) return value;
17
+ if (typeof value === "function") {
18
+ const reference = encodeActionReference(value);
19
+ if (reference) return reference;
20
+ const name = value.name;
21
+ throw new TypeError(`functions are not serializable${name ? ` (${name})` : ""}: pass a server action, or move the function into the client component`);
22
+ }
23
+ } });
24
+ return {
25
+ json: addressed.json,
26
+ values: selectValuesToSend(addressed, held).map(({ key, json }) => ({
27
+ key,
28
+ json
29
+ }))
30
+ };
31
+ },
32
+ escape: escapeScriptJson,
33
+ address(payload, skip) {
34
+ const addressed = addressValues(payload, { encoded: true });
35
+ return {
36
+ json: addressed.json,
37
+ values: selectValuesToSend(addressed, skip).map(({ key, json }) => ({
38
+ key,
39
+ json
40
+ }))
41
+ };
42
+ },
43
+ action: (value) => encodeActionReference(value)
44
+ });
45
+ /** The script carrying a form post's state to the browser, for `hydrateRoot`. */
46
+ function FormStateScript({ formState }) {
47
+ return React.createElement("script", {
48
+ type: FORM_STATE_SCRIPT_TYPE,
49
+ dangerouslySetInnerHTML: { __html: escapeScriptJson(JSON.stringify(encode(formState))) }
50
+ });
51
+ }
52
+ /**
53
+ * Renders a page whose client components are islands. A response rendered with a `request` whose
54
+ * browser holds values is that browser's alone: send it `private` and never share it.
55
+ *
56
+ * A navigation (`F-Render` on the request) gets the whole page in one piece: the browser puts it
57
+ * in place with DOM methods, which run no scripts, so the page cannot stream in with React's
58
+ * inline scripts. Every Suspense boundary has resolved before the first byte.
59
+ */
60
+ async function renderToStream(element, { request, formState, bootstrap = true, prehydration, ...options } = {}) {
61
+ const held = request ? parseHeldValues(request) : null;
62
+ const { render } = getContexts();
63
+ const tree = React.createElement(render.Provider, { value: {
64
+ held: held && held.size > 0 ? held : null,
65
+ sent: /* @__PURE__ */ new Set()
66
+ } }, element, formState ? React.createElement(FormStateScript, { formState }) : null);
67
+ const entry = bootstrap ? getBuildDefaults().entry : void 0;
68
+ const bootstrapModules = entry ? [...options.bootstrapModules ?? [], entry] : options.bootstrapModules;
69
+ const bootstrapScriptContent = prehydration ?? entry !== void 0 ? `${PREHYDRATION_SCRIPT}${options.bootstrapScriptContent ?? ""}` : options.bootstrapScriptContent;
70
+ const { renderToReadableStream } = await import("react-dom/server");
71
+ const stream = await renderToReadableStream(tree, {
72
+ ...options,
73
+ bootstrapModules,
74
+ bootstrapScriptContent,
75
+ formState
76
+ });
77
+ if (request?.headers.has("F-Render")) await stream.allReady;
78
+ return stream;
79
+ }
80
+ /** Whether a response rendered for `request` leaves out values its browser holds. */
81
+ function isPersonalized(request) {
82
+ return parseHeldValues(request).size > 0;
83
+ }
84
+ export { IslandsProvider, PAYLOAD_ONLY_ATTRIBUTE, createIsland, getClientReference, isPersonalized, isolateIslands, renderServerTree, renderToStream, setRenderStateResolver, withinClientRoot };
@@ -0,0 +1,3 @@
1
+ import { a as renderServerTree, c as IslandRenderState, f as isolateIslands, l as IslandsProvider, m as withinClientRoot, r as ServerTreeRenderOptions, u as createIsland } from "./_chunks/tree-fID6sz25.mjs";
2
+ import { n as IslandStrategy } from "./_chunks/schedule-Dw14dW0H.mjs";
3
+ export { type IslandRenderState, type IslandStrategy, IslandsProvider, type ServerTreeRenderOptions, createIsland, isolateIslands, renderServerTree, withinClientRoot };
package/dist/react.mjs ADDED
@@ -0,0 +1 @@
1
+ export { a as IslandsProvider, o as createIsland, l as isolateIslands, r as renderServerTree, p as withinClientRoot } from "./_chunks/tree-BDB5VEao.mjs";