@farm.js/islx 0.0.7 → 0.0.9
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 +41 -0
- package/README.md +56 -26
- package/SPEC.md +5 -4
- package/dist/_chunks/{actions-C-IwUCKU.mjs → actions-DAzd4PpV.mjs} +2 -2
- package/dist/_chunks/{actions-CvQ0-zjg.mjs → actions-DzIPDecd.mjs} +2 -2
- package/dist/_chunks/{codec-C8to-RQW.mjs → codec-DDpUvgRo.mjs} +1 -1
- package/dist/_chunks/{actions-Eutk6UzT.mjs → form-status-CRfnB7WT.mjs} +314 -1
- package/dist/_chunks/forms-CCDY9QHC.mjs +63 -0
- package/dist/_chunks/{tree-C47p6khN.mjs → island-viWIOM1k.mjs} +501 -8
- package/dist/_chunks/{prehydration-B1GLTLdk.mjs → prehydration-zEYsfFxW.mjs} +3 -2
- package/dist/_chunks/{schedule-CjoybS3b.mjs → schedule-IK9_RwZC.mjs} +13 -4
- package/dist/_chunks/{tree-B4RMU2UX.d.mts → tree-CV13RsJv.d.mts} +8 -1
- package/dist/_chunks/{values-37UIzxM0.mjs → values-gxhIei_m.mjs} +2 -2
- package/dist/_chunks/{vite-BTzYlEiY.mjs → vite-CHB6B8tP.mjs} +36 -12
- package/dist/_chunks/{wire-Dipqh-EB.d.mts → wire-1d6wqOia.d.mts} +16 -1
- package/dist/_chunks/{wire-QEQWBr5s.mjs → wire-UUwZlUJZ.mjs} +17 -1
- package/dist/boot.d.mts +31 -0
- package/dist/boot.mjs +177 -0
- package/dist/client.d.mts +2 -1
- package/dist/client.mjs +4 -28
- package/dist/compiler.d.mts +35 -1
- package/dist/compiler.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/react/client.d.mts +2 -2
- package/dist/react/client.mjs +88 -19
- package/dist/react/server.d.mts +1 -1
- package/dist/react/server.mjs +13 -10
- package/dist/react.d.mts +6 -2
- package/dist/react.mjs +22 -3
- package/dist/server.d.mts +5 -0
- package/dist/server.mjs +4 -4
- package/dist/tanstack-start/server.mjs +3 -3
- package/dist/tanstack-start.mjs +1 -1
- package/dist/vite.d.mts +16 -1
- package/dist/vite.mjs +1 -1
- package/package.json +5 -1
- package/dist/_chunks/island-DVOr-OUw.mjs +0 -262
|
@@ -1,8 +1,75 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
|
|
2
|
+
import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-DDpUvgRo.mjs";
|
|
3
|
+
import { D as SLOT_TEMPLATE_ELEMENT, F as VALUE_SCRIPT_TYPE, M as TREE_SCRIPT_TYPE, S as PROPS_SCRIPT_TYPE, T as SLOT_ELEMENT, b as PROMISE_SCRIPT_TYPE, c as CHUNK_SCRIPT_TYPE, g as ISLAND_ELEMENT, p as HTML_ELEMENT } from "./wire-UUwZlUJZ.mjs";
|
|
4
|
+
import { t as createServerReference, u as getValueStore } from "./actions-DAzd4PpV.mjs";
|
|
5
5
|
import * as React from "react";
|
|
6
|
+
import * as ReactDOM from "react-dom";
|
|
7
|
+
// src/react/ids.ts
|
|
8
|
+
/**
|
|
9
|
+
* `useId` in islands. React derives an id from the component's position in its root: every array
|
|
10
|
+
* of children on the way pushes a fork, `(slot << length) | bits`. The server renders a page as one
|
|
11
|
+
* root and the browser hydrates each island as a root of its own, so without help an island's ids
|
|
12
|
+
* start again in the browser, differ from its HTML, and repeat across islands.
|
|
13
|
+
*
|
|
14
|
+
* The server renders `[<IdProbe/>, <Component/>]` inside the island. The probe's id is the island's
|
|
15
|
+
* position with one more fork, slot 1 of 2: `(1 << length) | bits`, so its highest bit gives the
|
|
16
|
+
* position's length and the rest its bits. The browser rebuilds the same position with nested
|
|
17
|
+
* arrays (`null` in the slots before) and renders `[null, <Component/>]` there: React then computes
|
|
18
|
+
* the ids the server did, with the server's identifier prefix.
|
|
19
|
+
*/
|
|
20
|
+
/** Renders nothing; records the id React gives its position. */
|
|
21
|
+
function IdProbe({ record }) {
|
|
22
|
+
record.id = React.useId();
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const MAX_BITS = 30;
|
|
26
|
+
const MAX_FORK_WIDTH = 11;
|
|
27
|
+
/** The island's position, from the probe's id (`_<prefix>R_<tree id>_`), or null to skip. */
|
|
28
|
+
function parseIdPosition(id) {
|
|
29
|
+
const match = id ? /^_(.*)R_([0-9a-v]+)_$/.exec(id) : null;
|
|
30
|
+
if (!match) return null;
|
|
31
|
+
const probe = Number.parseInt(match[2], 32);
|
|
32
|
+
if (!Number.isSafeInteger(probe) || probe < 1) return null;
|
|
33
|
+
let length = 0;
|
|
34
|
+
while (2 ** (length + 1) <= probe) length++;
|
|
35
|
+
if (length + 2 > MAX_BITS) return null;
|
|
36
|
+
return {
|
|
37
|
+
prefix: match[1],
|
|
38
|
+
bits: probe - 2 ** length,
|
|
39
|
+
length
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `content` at `position`, as the second of two children, where the server rendered the island's
|
|
44
|
+
* component. Null when the position cannot be rebuilt.
|
|
45
|
+
*/
|
|
46
|
+
function atIdPosition(position, content) {
|
|
47
|
+
const { bits, length } = position;
|
|
48
|
+
const forks = [];
|
|
49
|
+
let start = 0;
|
|
50
|
+
for (let bit = 0; bit < length; bit++) if (Math.floor(bits / 2 ** bit) % 2 === 1) {
|
|
51
|
+
forks.push({
|
|
52
|
+
start,
|
|
53
|
+
width: bit - start + 1
|
|
54
|
+
});
|
|
55
|
+
start = bit + 1;
|
|
56
|
+
}
|
|
57
|
+
if (start < length) {
|
|
58
|
+
const last = forks.at(-1);
|
|
59
|
+
if (!last) return null;
|
|
60
|
+
last.width += length - start;
|
|
61
|
+
}
|
|
62
|
+
let node = React.createElement(React.Fragment, { key: "island" }, [null, React.createElement(React.Fragment, { key: "component" }, content)]);
|
|
63
|
+
for (let index = forks.length - 1; index >= 0; index--) {
|
|
64
|
+
const { start: from, width } = forks[index];
|
|
65
|
+
if (width > MAX_FORK_WIDTH) return null;
|
|
66
|
+
const slot = Math.floor(bits / 2 ** from) % 2 ** width;
|
|
67
|
+
const children = Array.from({ length: 2 ** (width - 1) }, () => null);
|
|
68
|
+
children[slot - 1] = node;
|
|
69
|
+
node = React.createElement(React.Fragment, { key: `fork${index}` }, children);
|
|
70
|
+
}
|
|
71
|
+
return node;
|
|
72
|
+
}
|
|
6
73
|
// src/react/tree.ts
|
|
7
74
|
/**
|
|
8
75
|
* Server trees: a page as a serialized React tree, the role React Server Components' Flight
|
|
@@ -100,7 +167,7 @@ function callServerComponent(render, props, forwardRef) {
|
|
|
100
167
|
slot?.set(previous);
|
|
101
168
|
}
|
|
102
169
|
}
|
|
103
|
-
function isThenable(value) {
|
|
170
|
+
function isThenable$1(value) {
|
|
104
171
|
return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
105
172
|
}
|
|
106
173
|
function serverHtmlNode(state, node, key, context) {
|
|
@@ -146,7 +213,7 @@ async function resolveValue(state, value, context, seen) {
|
|
|
146
213
|
}
|
|
147
214
|
if (typeof value !== "object") throw new UnsupportedNode(`passes a ${typeof value} value`);
|
|
148
215
|
if (React.isValidElement(value)) return resolveElement(state, value, context);
|
|
149
|
-
if (isThenable(value)) try {
|
|
216
|
+
if (isThenable$1(value)) try {
|
|
150
217
|
return {
|
|
151
218
|
$: "P",
|
|
152
219
|
v: await resolveValue(state, await value, context, seen)
|
|
@@ -261,7 +328,7 @@ async function resolveElement(state, element, context) {
|
|
|
261
328
|
if (typeof type !== "function") throw new UnsupportedNode("is not a function component");
|
|
262
329
|
if (type.prototype?.isReactComponent) throw new UnsupportedNode("is a class component");
|
|
263
330
|
let rendered = callServerComponent(type, props, forwardRef);
|
|
264
|
-
if (isThenable(rendered)) rendered = await rendered;
|
|
331
|
+
if (isThenable$1(rendered)) rendered = await rendered;
|
|
265
332
|
const resolved = await resolveValue(state, rendered, context, /* @__PURE__ */ new Set());
|
|
266
333
|
return key === null ? resolved : {
|
|
267
334
|
$: "f",
|
|
@@ -687,4 +754,430 @@ async function createServerTreeLayout(source, scope, options) {
|
|
|
687
754
|
}
|
|
688
755
|
};
|
|
689
756
|
}
|
|
690
|
-
|
|
757
|
+
/** Whether a resolved tree needs the browser to rebuild it: it holds anything but plain HTML. */
|
|
758
|
+
function needsRebuild(value) {
|
|
759
|
+
if (value === null || typeof value !== "object") return false;
|
|
760
|
+
if (Array.isArray(value)) return value.some(needsRebuild);
|
|
761
|
+
if (value.$ === "c" || value.$ === "p" || value.$ === "x" || value.$ === "a") return true;
|
|
762
|
+
return Object.values(value).some(needsRebuild);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* @internal Resolves server output passed to an island. Plain HTML renders as it is, and the
|
|
766
|
+
* browser keeps it. Output holding client components (or Suspense, actions, server HTML parts)
|
|
767
|
+
* renders with its payload, scoped to the slot's name, so the browser rebuilds it inside the
|
|
768
|
+
* island's root: its client components are then the island's descendants and read its context,
|
|
769
|
+
* as in React Server Components.
|
|
770
|
+
*/
|
|
771
|
+
async function renderSlotTree(element, scope, held) {
|
|
772
|
+
const hook = getIslandServerHook();
|
|
773
|
+
if (!hook) throw new Error("islx: an island rendered without @farm.js/islx/react/server");
|
|
774
|
+
const state = {
|
|
775
|
+
nextId: 0,
|
|
776
|
+
components: /* @__PURE__ */ new Map(),
|
|
777
|
+
serverHtml: /* @__PURE__ */ new Map(),
|
|
778
|
+
chunks: /* @__PURE__ */ new Map(),
|
|
779
|
+
actions: /* @__PURE__ */ new Map()
|
|
780
|
+
};
|
|
781
|
+
const tree = await resolveValue(state, element, { parserSensitive: false }, /* @__PURE__ */ new Set());
|
|
782
|
+
const encode = (payload) => {
|
|
783
|
+
const addressed = hook.address(payload, held);
|
|
784
|
+
return {
|
|
785
|
+
json: hook.escape(addressed.json),
|
|
786
|
+
values: addressed.values.map((value) => [value.key, hook.escape(value.json)])
|
|
787
|
+
};
|
|
788
|
+
};
|
|
789
|
+
const resolver = {
|
|
790
|
+
component(reference, exportName) {
|
|
791
|
+
const Component = state.components.get(`${reference}#${exportName}`);
|
|
792
|
+
if (!Component) throw new Error(`client reference ${reference}#${exportName} was not resolved`);
|
|
793
|
+
return Component;
|
|
794
|
+
},
|
|
795
|
+
chunk(id) {
|
|
796
|
+
const entry = state.chunks.get(id);
|
|
797
|
+
if (entry.chunk) return entry.chunk;
|
|
798
|
+
const chunk = {
|
|
799
|
+
status: "pending",
|
|
800
|
+
promise: entry.encoded.then((encoded) => {
|
|
801
|
+
const payload = encode({ t: encoded });
|
|
802
|
+
chunk.value = React.createElement(React.Fragment, null, createTreeValue(encoded, resolver), createTreeScript(CHUNK_SCRIPT_TYPE, scopedName(scope, id), payload.json, payload.values));
|
|
803
|
+
chunk.status = "fulfilled";
|
|
804
|
+
return chunk.value;
|
|
805
|
+
}, (error) => {
|
|
806
|
+
chunk.status = "rejected";
|
|
807
|
+
chunk.reason = error;
|
|
808
|
+
throw error;
|
|
809
|
+
})
|
|
810
|
+
};
|
|
811
|
+
chunk.promise.catch(() => {});
|
|
812
|
+
entry.chunk = chunk;
|
|
813
|
+
return chunk;
|
|
814
|
+
},
|
|
815
|
+
serverHtml(id, key) {
|
|
816
|
+
return React.createElement(SERVER_HTML_TAG, {
|
|
817
|
+
...key === null ? {} : { key },
|
|
818
|
+
[SERVER_HTML_ATTRIBUTE]: scopedName(scope, id),
|
|
819
|
+
style: { display: "contents" }
|
|
820
|
+
}, isolateIslands(state.serverHtml.get(id)));
|
|
821
|
+
},
|
|
822
|
+
outlet() {
|
|
823
|
+
return null;
|
|
824
|
+
},
|
|
825
|
+
action(id) {
|
|
826
|
+
return state.actions.get(id);
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
const rendered = createTreeValue(tree, resolver);
|
|
830
|
+
if (!needsRebuild(tree)) return {
|
|
831
|
+
element: React.createElement(React.Fragment, null, rendered),
|
|
832
|
+
rebuild: false
|
|
833
|
+
};
|
|
834
|
+
const payload = encode({
|
|
835
|
+
v: 1,
|
|
836
|
+
t: tree
|
|
837
|
+
});
|
|
838
|
+
return {
|
|
839
|
+
element: React.createElement(React.Fragment, null, rendered, createTreeScript(TREE_SCRIPT_TYPE, scope, payload.json, payload.values)),
|
|
840
|
+
rebuild: true
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* @internal Rebuilds a slot the server rendered with `renderSlotTree`, from `root` (the slot's
|
|
845
|
+
* element, or the template holding it) after loading its client modules. Null when the slot has no
|
|
846
|
+
* payload there.
|
|
847
|
+
*/
|
|
848
|
+
async function createSlotTree(root, scope, options) {
|
|
849
|
+
const tree = await prepareTree(root, scope, options);
|
|
850
|
+
if (!tree) return null;
|
|
851
|
+
const resolver = tree.resolver();
|
|
852
|
+
const script = createTreeScript(TREE_SCRIPT_TYPE, scope, tree.json, tree.values);
|
|
853
|
+
const build = () => React.createElement(React.Fragment, null, createTreeValue(tree.payload, resolver), script);
|
|
854
|
+
build();
|
|
855
|
+
return build();
|
|
856
|
+
}
|
|
857
|
+
// src/react/island.ts
|
|
858
|
+
/**
|
|
859
|
+
* A client component, as the server renders it: its HTML inside `<f-island>`, and its props in
|
|
860
|
+
* a script after it. Shared by the server and the browser: inside a hydrating island, a nested
|
|
861
|
+
* client component is part of that island's React tree, not an island of its own.
|
|
862
|
+
*
|
|
863
|
+
* The compiler wraps every export of a `"use client"` module with `createIsland` in the server
|
|
864
|
+
* build. The browser build keeps the module as it is.
|
|
865
|
+
*/
|
|
866
|
+
const CLIENT_REFERENCE = Symbol.for("islx.react.client-reference");
|
|
867
|
+
/** The module export an island component renders, or undefined for any other type. */
|
|
868
|
+
function getClientReference(type) {
|
|
869
|
+
if (!type || typeof type !== "function" && typeof type !== "object") return void 0;
|
|
870
|
+
return type[CLIENT_REFERENCE];
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Makes `element`'s client components part of the React root around it (a layout that hydrates
|
|
874
|
+
* as one root), instead of islands of their own.
|
|
875
|
+
*/
|
|
876
|
+
function withinClientRoot(element) {
|
|
877
|
+
return React.createElement(getContexts().graph.Provider, { value: true }, element);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Starts islands again below a `withinClientRoot`: the page a hydrating layout keeps as server
|
|
881
|
+
* HTML renders its client components as islands of their own.
|
|
882
|
+
*/
|
|
883
|
+
function isolateIslands(element) {
|
|
884
|
+
return React.createElement(getContexts().graph.Provider, { value: false }, element);
|
|
885
|
+
}
|
|
886
|
+
const RENDER_STATE_RESOLVER = Symbol.for("islx.react.render-state");
|
|
887
|
+
/**
|
|
888
|
+
* For a framework that knows the request being rendered without React context (a request store):
|
|
889
|
+
* returns the render's settings when no `IslandsProvider` or `renderToStream` gave them. Keep the
|
|
890
|
+
* same `sent` set for the whole response.
|
|
891
|
+
*/
|
|
892
|
+
function setRenderStateResolver(resolve) {
|
|
893
|
+
globalThis[RENDER_STATE_RESOLVER] = resolve;
|
|
894
|
+
}
|
|
895
|
+
function resolveRenderState() {
|
|
896
|
+
const resolve = globalThis[RENDER_STATE_RESOLVER];
|
|
897
|
+
return resolve ? resolve() : null;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Gives islands a render's settings when the framework renders the page itself (not through
|
|
901
|
+
* `renderToStream`): which values the browser holds, and which this render already wrote.
|
|
902
|
+
*/
|
|
903
|
+
function IslandsProvider({ held = null, children }) {
|
|
904
|
+
const state = React.useMemo(() => ({
|
|
905
|
+
held: held && held.size > 0 ? held : null,
|
|
906
|
+
sent: /* @__PURE__ */ new Set()
|
|
907
|
+
}), [held]);
|
|
908
|
+
return React.createElement(getContexts().render.Provider, { value: state }, children);
|
|
909
|
+
}
|
|
910
|
+
const SERVER_HOOK = Symbol.for("islx.react.server");
|
|
911
|
+
/** @internal */
|
|
912
|
+
function getIslandServerHook() {
|
|
913
|
+
return globalThis[SERVER_HOOK];
|
|
914
|
+
}
|
|
915
|
+
/** @internal */
|
|
916
|
+
function setIslandServerHook(hook) {
|
|
917
|
+
globalThis[SERVER_HOOK] = hook;
|
|
918
|
+
}
|
|
919
|
+
/** Tagged objects the runtime made (slots), as opposed to app data with a "$" key. */
|
|
920
|
+
const runtimeTagged = /* @__PURE__ */ new WeakSet();
|
|
921
|
+
const CONTEXTS = Symbol.for("islx.react.contexts");
|
|
922
|
+
/** @internal */
|
|
923
|
+
function getContexts() {
|
|
924
|
+
const scope = globalThis;
|
|
925
|
+
const all = scope[CONTEXTS] ??= /* @__PURE__ */ new WeakMap();
|
|
926
|
+
let contexts = all.get(React.createElement);
|
|
927
|
+
if (!contexts) {
|
|
928
|
+
contexts = {
|
|
929
|
+
graph: React.createContext(false),
|
|
930
|
+
render: React.createContext(null),
|
|
931
|
+
container: React.createContext(null)
|
|
932
|
+
};
|
|
933
|
+
all.set(React.createElement, contexts);
|
|
934
|
+
}
|
|
935
|
+
return contexts;
|
|
936
|
+
}
|
|
937
|
+
function isElement(value) {
|
|
938
|
+
const type = value?.$$typeof;
|
|
939
|
+
return type === Symbol.for("react.transitional.element") || type === Symbol.for("react.element");
|
|
940
|
+
}
|
|
941
|
+
const slotStyle = { display: "contents" };
|
|
942
|
+
const resolvedSlots = /* @__PURE__ */ new WeakMap();
|
|
943
|
+
function resolveSlots(props, elements, held) {
|
|
944
|
+
let resolved = resolvedSlots.get(props);
|
|
945
|
+
if (!resolved) {
|
|
946
|
+
resolved = Promise.all(Array.from(elements, async ([name, element]) => [name, await renderSlotTree(element, name, held)])).then((entries) => new Map(entries));
|
|
947
|
+
resolvedSlots.set(props, resolved);
|
|
948
|
+
}
|
|
949
|
+
return resolved;
|
|
950
|
+
}
|
|
951
|
+
/** Server output in an island, where the island placed it. */
|
|
952
|
+
function Slot({ name, slots }) {
|
|
953
|
+
slots.placed.add(name);
|
|
954
|
+
return React.createElement(SLOT_ELEMENT, {
|
|
955
|
+
name,
|
|
956
|
+
style: slotStyle
|
|
957
|
+
}, slots.trees.get(name)?.element);
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Splits props into what renders on the server and what travels: server output passed to an
|
|
961
|
+
* island (`children`, or any element prop) renders once inside `<f-slot>`, and travels as a
|
|
962
|
+
* reference to it, so the browser reads its HTML from the page instead of receiving it twice.
|
|
963
|
+
*/
|
|
964
|
+
function splitSlots(value, prefix, slots) {
|
|
965
|
+
if (isElement(value)) {
|
|
966
|
+
const name = `${prefix}${slots.next++}`;
|
|
967
|
+
const reference = {
|
|
968
|
+
["$"]: "s",
|
|
969
|
+
["v"]: name
|
|
970
|
+
};
|
|
971
|
+
runtimeTagged.add(reference);
|
|
972
|
+
slots.elements.set(name, value);
|
|
973
|
+
slots.references.set(name, reference);
|
|
974
|
+
return {
|
|
975
|
+
render: React.createElement(Slot, {
|
|
976
|
+
key: value.key ?? name,
|
|
977
|
+
name,
|
|
978
|
+
slots
|
|
979
|
+
}),
|
|
980
|
+
wire: reference
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
if (Array.isArray(value)) {
|
|
984
|
+
const parts = value.map((entry) => splitSlots(entry, prefix, slots));
|
|
985
|
+
return {
|
|
986
|
+
render: parts.map((part) => part.render),
|
|
987
|
+
wire: parts.map((part) => part.wire)
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
if (value && typeof value === "object") {
|
|
991
|
+
const prototype = Object.getPrototypeOf(value);
|
|
992
|
+
if (prototype !== Object.prototype && prototype !== null) return {
|
|
993
|
+
render: value,
|
|
994
|
+
wire: value
|
|
995
|
+
};
|
|
996
|
+
const render = {};
|
|
997
|
+
const wire = {};
|
|
998
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
999
|
+
const part = splitSlots(entry, prefix, slots);
|
|
1000
|
+
render[key] = part.render;
|
|
1001
|
+
wire[key] = part.wire;
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
render,
|
|
1005
|
+
wire
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
render: value,
|
|
1010
|
+
wire: value
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
const warned = /* @__PURE__ */ new Set();
|
|
1014
|
+
const outcomes = /* @__PURE__ */ new WeakMap();
|
|
1015
|
+
function settle(promise) {
|
|
1016
|
+
let outcome = outcomes.get(promise);
|
|
1017
|
+
if (!outcome) {
|
|
1018
|
+
outcome = Promise.resolve(promise).then((value) => ({
|
|
1019
|
+
ok: true,
|
|
1020
|
+
value
|
|
1021
|
+
}), (reason) => ({
|
|
1022
|
+
ok: false,
|
|
1023
|
+
reason
|
|
1024
|
+
}));
|
|
1025
|
+
outcomes.set(promise, outcome);
|
|
1026
|
+
}
|
|
1027
|
+
return outcome;
|
|
1028
|
+
}
|
|
1029
|
+
const REJECTED = "{\"e\":1}";
|
|
1030
|
+
/** Each promise an island received, in a boundary of its own: its outcome once it settles. */
|
|
1031
|
+
function promiseBoundaries(promises, label) {
|
|
1032
|
+
return promises.map(({ name, promise }) => React.createElement(React.Suspense, {
|
|
1033
|
+
key: name,
|
|
1034
|
+
fallback: null
|
|
1035
|
+
}, React.createElement(IslandPromise, {
|
|
1036
|
+
name,
|
|
1037
|
+
promise,
|
|
1038
|
+
label
|
|
1039
|
+
})));
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* A promise's outcome, written when it settles. Its values are sent whether or not the page wrote
|
|
1043
|
+
* them already: this script may land before an earlier island's, wherever React puts it.
|
|
1044
|
+
*/
|
|
1045
|
+
function IslandPromise({ name, promise, label }) {
|
|
1046
|
+
const outcome = React.use(settle(promise));
|
|
1047
|
+
const state = React.useContext(getContexts().render) ?? resolveRenderState();
|
|
1048
|
+
const hook = getIslandServerHook();
|
|
1049
|
+
let encoded = {
|
|
1050
|
+
json: REJECTED,
|
|
1051
|
+
values: [],
|
|
1052
|
+
promises: []
|
|
1053
|
+
};
|
|
1054
|
+
if (outcome.ok) try {
|
|
1055
|
+
encoded = hook.encode({ v: outcome.value }, state?.held ?? null);
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
console.warn(`[islx] A promise passed to ${label} resolved to a value that cannot be sent to the browser: it rejects there.`, error);
|
|
1058
|
+
}
|
|
1059
|
+
else console.error(`[islx] A promise passed to ${label} rejected.`, outcome.reason);
|
|
1060
|
+
return React.createElement(React.Fragment, null, ...encoded.values.map((value) => React.createElement("script", {
|
|
1061
|
+
key: value.key,
|
|
1062
|
+
type: VALUE_SCRIPT_TYPE,
|
|
1063
|
+
name: value.key,
|
|
1064
|
+
dangerouslySetInnerHTML: { __html: hook.escape(value.json) }
|
|
1065
|
+
})), React.createElement("script", {
|
|
1066
|
+
key: "outcome",
|
|
1067
|
+
type: PROMISE_SCRIPT_TYPE,
|
|
1068
|
+
name,
|
|
1069
|
+
dangerouslySetInnerHTML: { __html: hook.escape(encoded.json) }
|
|
1070
|
+
}), ...promiseBoundaries(encoded.promises, label));
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* The island's props. It renders after the island, so by then the island's id position (`ids`) is
|
|
1074
|
+
* known, and which of its element props it placed. The others (a closed panel, a tab not shown
|
|
1075
|
+
* yet) render once in a `<template>` after the props, which the browser reads them from when the
|
|
1076
|
+
* island shows them; a slot placed later, by a part of the island that suspended first, renders in
|
|
1077
|
+
* both places.
|
|
1078
|
+
*/
|
|
1079
|
+
function PropsScript({ id, ids, slots, json }) {
|
|
1080
|
+
const script = React.createElement("script", {
|
|
1081
|
+
key: "props",
|
|
1082
|
+
type: PROPS_SCRIPT_TYPE,
|
|
1083
|
+
htmlFor: id,
|
|
1084
|
+
["ids"]: ids.id,
|
|
1085
|
+
dangerouslySetInnerHTML: { __html: json }
|
|
1086
|
+
});
|
|
1087
|
+
const unplaced = [...slots.elements].filter(([name]) => !slots.placed.has(name));
|
|
1088
|
+
if (unplaced.length === 0) return script;
|
|
1089
|
+
return React.createElement(React.Fragment, null, script, React.createElement(SLOT_TEMPLATE_ELEMENT, {
|
|
1090
|
+
key: "slots",
|
|
1091
|
+
htmlFor: id
|
|
1092
|
+
}, unplaced.map(([name]) => React.createElement(SLOT_ELEMENT, {
|
|
1093
|
+
key: name,
|
|
1094
|
+
name,
|
|
1095
|
+
style: slotStyle
|
|
1096
|
+
}, slots.trees.get(name)?.element))));
|
|
1097
|
+
}
|
|
1098
|
+
function isThenable(value) {
|
|
1099
|
+
return !!value && typeof value.then === "function";
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Wraps a client component so the server renders it as an island. `src` is the module the browser
|
|
1103
|
+
* imports, `exportName` the component's export there.
|
|
1104
|
+
*/
|
|
1105
|
+
function createIsland(Component, src, exportName, declared = "load", options = {}) {
|
|
1106
|
+
function Island(props) {
|
|
1107
|
+
const { graph, render } = getContexts();
|
|
1108
|
+
const insideIsland = React.useContext(graph);
|
|
1109
|
+
const state = React.useContext(render) ?? resolveRenderState();
|
|
1110
|
+
if (insideIsland) return React.createElement(Component, props);
|
|
1111
|
+
const id = React.useId();
|
|
1112
|
+
const hook = getIslandServerHook();
|
|
1113
|
+
if (!hook) throw new Error("islx: an island rendered on a server that did not import @farm.js/islx/react/server");
|
|
1114
|
+
let encoded;
|
|
1115
|
+
let strategy = declared;
|
|
1116
|
+
const slots = {
|
|
1117
|
+
next: 0,
|
|
1118
|
+
elements: /* @__PURE__ */ new Map(),
|
|
1119
|
+
references: /* @__PURE__ */ new Map(),
|
|
1120
|
+
placed: /* @__PURE__ */ new Set(),
|
|
1121
|
+
trees: /* @__PURE__ */ new Map()
|
|
1122
|
+
};
|
|
1123
|
+
const { render: renderProps, wire } = splitSlots(props, `${id}s`, slots);
|
|
1124
|
+
if (slots.elements.size > 0) {
|
|
1125
|
+
slots.trees = React.use(resolveSlots(props, slots.elements, state?.held ?? null));
|
|
1126
|
+
for (const [name, reference] of slots.references) if (slots.trees.get(name)?.rebuild) reference["t"] = 1;
|
|
1127
|
+
}
|
|
1128
|
+
try {
|
|
1129
|
+
if (options.inferred && slots.next > 0) strategy = "load";
|
|
1130
|
+
const skip = state ? /* @__PURE__ */ new Set([...state.held ?? [], ...state.sent]) : null;
|
|
1131
|
+
encoded = hook.encode(wire, skip);
|
|
1132
|
+
for (const value of encoded.values) state?.sent.add(value.key);
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
if (isThenable(error)) throw error;
|
|
1135
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1136
|
+
const key = `${src}#${exportName}:${message}`;
|
|
1137
|
+
if (!warned.has(key)) {
|
|
1138
|
+
warned.add(key);
|
|
1139
|
+
console.warn(`[islx] ${exportName} (${src}) was rendered without hydration: its props cannot be sent to the browser. ${message}`);
|
|
1140
|
+
}
|
|
1141
|
+
return React.createElement(Component, props);
|
|
1142
|
+
}
|
|
1143
|
+
const ids = {};
|
|
1144
|
+
if (strategy === "load") {
|
|
1145
|
+
const defaults = getBuildDefaults();
|
|
1146
|
+
if (defaults.boot && defaults.entry) ReactDOM.preloadModule?.(defaults.entry, { as: "script" });
|
|
1147
|
+
for (const href of defaults.modules?.[src] ?? []) ReactDOM.preloadModule?.(href, { as: "script" });
|
|
1148
|
+
}
|
|
1149
|
+
return React.createElement(React.Fragment, null, ...encoded.values.map((value) => React.createElement("script", {
|
|
1150
|
+
key: value.key,
|
|
1151
|
+
type: VALUE_SCRIPT_TYPE,
|
|
1152
|
+
name: value.key,
|
|
1153
|
+
dangerouslySetInnerHTML: { __html: hook.escape(value.json) }
|
|
1154
|
+
})), React.createElement(ISLAND_ELEMENT, {
|
|
1155
|
+
key: "island",
|
|
1156
|
+
src,
|
|
1157
|
+
export: exportName,
|
|
1158
|
+
props: id,
|
|
1159
|
+
strategy,
|
|
1160
|
+
style: slotStyle
|
|
1161
|
+
}, React.createElement(graph.Provider, { value: true }, [React.createElement(IdProbe, {
|
|
1162
|
+
key: "ids",
|
|
1163
|
+
record: ids
|
|
1164
|
+
}), React.createElement(Component, {
|
|
1165
|
+
...renderProps,
|
|
1166
|
+
key: "component"
|
|
1167
|
+
})])), React.createElement(PropsScript, {
|
|
1168
|
+
key: "props",
|
|
1169
|
+
id,
|
|
1170
|
+
ids,
|
|
1171
|
+
slots,
|
|
1172
|
+
json: hook.escape(encoded.json)
|
|
1173
|
+
}), ...promiseBoundaries(encoded.promises, `${exportName} (${src})`));
|
|
1174
|
+
}
|
|
1175
|
+
Island.displayName = `Island(${Component.displayName || Component.name || exportName})`;
|
|
1176
|
+
Object.defineProperty(Island, CLIENT_REFERENCE, { value: {
|
|
1177
|
+
src,
|
|
1178
|
+
exportName,
|
|
1179
|
+
Component
|
|
1180
|
+
} });
|
|
1181
|
+
return Island;
|
|
1182
|
+
}
|
|
1183
|
+
export { atIdPosition as _, isolateIslands as a, setRenderStateResolver as c, PAYLOAD_ONLY_ATTRIBUTE as d, createServerTreeElement as f, updateServerTrees as g, renderServerTree as h, getContexts as i, withinClientRoot as l, createSlotTree as m, createIsland as n, runtimeTagged as o, createServerTreeLayout as p, getClientReference as r, setIslandServerHook as s, IslandsProvider as t, Outlet as u, parseIdPosition as v };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/client/prehydration.ts
|
|
2
2
|
/**
|
|
3
3
|
* Holding interactions that happen before the islx script has run. An inline script in the page
|
|
4
|
-
* captures a click or a submit on an island that has not hydrated (it is prevented and queued
|
|
4
|
+
* captures a click or a submit on an island that has not hydrated (it is prevented and queued; a
|
|
5
|
+
* click on a link only when the link stays on the page, `#…`),
|
|
5
6
|
* and announces a pointerdown or a focus (which go through). The runtime then hydrates those
|
|
6
7
|
* islands first and replays what was held.
|
|
7
8
|
*
|
|
@@ -14,7 +15,7 @@ const INTERACTION_EVENT = "f:island-interaction";
|
|
|
14
15
|
/** Elements whose click is held and replayed rather than merely observed. */
|
|
15
16
|
const REPLAYABLE_SELECTOR = "button,[role=\"button\"],input[type=\"button\"],input[type=\"submit\"],input[type=\"reset\"]";
|
|
16
17
|
/** The inline script's source (no `<script>` tag): pass it to the page's head or bootstrap. */
|
|
17
|
-
const PREHYDRATION_SCRIPT = `(function(){if(window.${PENDING_INTERACTIONS})return;var q=window.${PENDING_INTERACTIONS}=[];function island(n){var i=n&&n.closest&&n.closest("f-island");return i&&!i.hasAttribute("hydrated")?i:null}function tell(t,k){document.dispatchEvent(new CustomEvent("${INTERACTION_EVENT}",{detail:{target:t,kind:k}}))}function hold(e,t,k){if(!q.some(function(p){return p.target===t})){q.push({target:t,kind:k});tell(t,k)}e.preventDefault();e.stopImmediatePropagation()}document.addEventListener("click",function(e){if(e.defaultPrevented||e.button!==0||e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)return;var
|
|
18
|
+
const PREHYDRATION_SCRIPT = `(function(){if(window.${PENDING_INTERACTIONS})return;var q=window.${PENDING_INTERACTIONS}=[];function island(n){var i=n&&n.closest&&n.closest("f-island");return i&&!i.hasAttribute("hydrated")?i:null}function tell(t,k){document.dispatchEvent(new CustomEvent("${INTERACTION_EVENT}",{detail:{target:t,kind:k}}))}function hold(e,t,k){if(!q.some(function(p){return p.target===t})){q.push({target:t,kind:k});tell(t,k)}e.preventDefault();e.stopImmediatePropagation()}document.addEventListener("click",function(e){if(e.defaultPrevented||e.button!==0||e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)return;var n=e.target&&e.target.closest?e.target:null,a=n&&n.closest("a[href]"),t=a?(a.getAttribute("href").charAt(0)==="#"?a:null):n&&n.closest(${JSON.stringify(REPLAYABLE_SELECTOR)});if(!t||!island(t))return;hold(e,t,"click")},true);document.addEventListener("submit",function(e){var f=e.target;if(e.defaultPrevented||!f||f.nodeName!=="FORM"||!island(f))return;hold(e,f,"submit")},true);function seen(e){if(island(e.target))tell(e.target,null)}document.addEventListener("pointerdown",seen,true);document.addEventListener("focusin",seen,true)})();`;
|
|
18
19
|
/** Takes the interactions the inline script held inside `container`, oldest first. */
|
|
19
20
|
function takePendingInteractions(container) {
|
|
20
21
|
const queue = globalThis[PENDING_INTERACTIONS];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as hasPendingInteraction, o as takePendingInteractions, t as INTERACTION_EVENT } from "./prehydration-
|
|
2
|
-
import { m as HYDRATED_ATTRIBUTE } from "./wire-
|
|
1
|
+
import { a as hasPendingInteraction, o as takePendingInteractions, t as INTERACTION_EVENT } from "./prehydration-zEYsfFxW.mjs";
|
|
2
|
+
import { m as HYDRATED_ATTRIBUTE } from "./wire-UUwZlUJZ.mjs";
|
|
3
3
|
// src/client/schedule.ts
|
|
4
4
|
/**
|
|
5
5
|
* When an island hydrates. Renderer-neutral: every adapter schedules through this.
|
|
@@ -24,6 +24,15 @@ function isIslandStrategy(value) {
|
|
|
24
24
|
}
|
|
25
25
|
/** Elements whose click is held and replayed rather than merely observed. */
|
|
26
26
|
const REPLAYABLE = "button,[role=\"button\"],input[type=\"button\"],input[type=\"submit\"],input[type=\"reset\"]";
|
|
27
|
+
/**
|
|
28
|
+
* The element a click before hydration is held on: a control, or a same-page link, whose handler
|
|
29
|
+
* the island may need (a table of contents marking its heading). Any other link navigates.
|
|
30
|
+
*/
|
|
31
|
+
function heldTarget(target) {
|
|
32
|
+
const link = target.closest("a[href]");
|
|
33
|
+
if (link) return link.getAttribute("href")?.startsWith("#") ? link : null;
|
|
34
|
+
return target.closest(REPLAYABLE);
|
|
35
|
+
}
|
|
27
36
|
function whenIdle(callback) {
|
|
28
37
|
const scope = window;
|
|
29
38
|
if (typeof scope.requestIdleCallback === "function") {
|
|
@@ -190,8 +199,8 @@ function scheduleHydration({ container, strategy, signal, hydrate }) {
|
|
|
190
199
|
};
|
|
191
200
|
const onClick = (event) => {
|
|
192
201
|
const mouse = event;
|
|
193
|
-
const target = event.target instanceof Element ? event.target
|
|
194
|
-
if (!target || !container.contains(target) ||
|
|
202
|
+
const target = event.target instanceof Element ? heldTarget(event.target) : null;
|
|
203
|
+
if (!target || !container.contains(target) || mouse.button !== 0 || mouse.metaKey || mouse.altKey || mouse.ctrlKey || mouse.shiftKey) return;
|
|
195
204
|
hold(event, target, "click");
|
|
196
205
|
};
|
|
197
206
|
const onSubmit = (event) => {
|
|
@@ -52,7 +52,14 @@ interface IslandRenderState {
|
|
|
52
52
|
* Wraps a client component so the server renders it as an island. `src` is the module the browser
|
|
53
53
|
* imports, `exportName` the component's export there.
|
|
54
54
|
*/
|
|
55
|
-
declare function createIsland<P extends object>(Component: React.ComponentType<P>, src: string, exportName: string,
|
|
55
|
+
declare function createIsland<P extends object>(Component: React.ComponentType<P>, src: string, exportName: string, declared?: IslandStrategy,
|
|
56
|
+
/**
|
|
57
|
+
* `inferred`: the compiler inferred `declared` (`"interaction"`). A render that passes the island
|
|
58
|
+
* server output hydrates at load instead: islands inside that output would wait with it.
|
|
59
|
+
*/
|
|
60
|
+
options?: {
|
|
61
|
+
inferred?: boolean;
|
|
62
|
+
}): React.ComponentType<P>;
|
|
56
63
|
/**
|
|
57
64
|
* Marks a navigation response that carries only the page's payload. The browser rebuilds such a
|
|
58
65
|
* page in its layout's React root and never shows its HTML, so the server leaves the HTML out.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as needsObjectTag, o as encodeLeaf } from "./codec-
|
|
2
|
-
import {
|
|
1
|
+
import { c as needsObjectTag, o as encodeLeaf } from "./codec-DDpUvgRo.mjs";
|
|
2
|
+
import { N as VALUES_HEADER, P as VALUE_KEY_PATTERN } from "./wire-UUwZlUJZ.mjs";
|
|
3
3
|
// src/server/values.ts
|
|
4
4
|
/**
|
|
5
5
|
* Values sent once: which parts of an island's props travel as values of their own, keyed by a
|