@farm.js/islx 0.0.8 → 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 +19 -0
- package/README.md +1 -1
- 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/{infer-BIVT5aDz.mjs → form-status-CRfnB7WT.mjs} +46 -1
- package/dist/_chunks/forms-CCDY9QHC.mjs +63 -0
- package/dist/_chunks/{tree-BQphP23Y.mjs → island-viWIOM1k.mjs} +501 -8
- package/dist/_chunks/{schedule-Cwn9vQL0.mjs → schedule-IK9_RwZC.mjs} +1 -1
- package/dist/_chunks/{values-37UIzxM0.mjs → values-gxhIei_m.mjs} +2 -2
- package/dist/_chunks/{vite-0bH_ZDys.mjs → vite-CHB6B8tP.mjs} +11 -2
- 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/client.mjs +3 -27
- package/dist/compiler.d.mts +13 -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 +1 -1
- package/dist/react/client.mjs +88 -19
- package/dist/react/server.mjs +5 -6
- package/dist/react.d.mts +5 -1
- package/dist/react.mjs +22 -3
- package/dist/server.mjs +4 -4
- package/dist/tanstack-start/server.mjs +3 -3
- package/dist/tanstack-start.mjs +1 -1
- package/dist/vite.mjs +1 -1
- package/package.json +1 -1
- package/dist/_chunks/island-DXAGP4FN.mjs +0 -269
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
import { a as hasPendingInteraction, o as takePendingInteractions, t as INTERACTION_EVENT } from "./prehydration-zEYsfFxW.mjs";
|
|
2
|
-
import { m as HYDRATED_ATTRIBUTE } from "./wire-
|
|
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.
|
|
@@ -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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as ISLAND_STRATEGIES } from "./schedule-
|
|
1
|
+
import { i as ActionCompileError, n as rewriteFormStatusImports, o as hasModuleDirective, r as inferStrategy, s as transformActions, t as rewriteFormStatusImport } from "./form-status-CRfnB7WT.mjs";
|
|
2
|
+
import { t as ISLAND_STRATEGIES } from "./schedule-IK9_RwZC.mjs";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { walk } from "estree-walker";
|
|
@@ -86,6 +86,7 @@ function transformClientModule(options) {
|
|
|
86
86
|
return configured ?? strategies.byName.get(name) ?? strategies.all ?? (inference?.strategy === "interaction" ? "interaction" : "load");
|
|
87
87
|
};
|
|
88
88
|
const output = new MagicString(code);
|
|
89
|
+
rewriteFormStatusImport(program, code, output, options.runtime ?? "@farm.js/islx/react");
|
|
89
90
|
const wrapped = [];
|
|
90
91
|
for (const node of program.body ?? []) if (node.type === "ExportNamedDeclaration" && !node.source && node.exportKind !== "type") {
|
|
91
92
|
const declaration = node.declaration;
|
|
@@ -460,6 +461,14 @@ function islx(options = {}, host = {}) {
|
|
|
460
461
|
}
|
|
461
462
|
}
|
|
462
463
|
if (library) return null;
|
|
464
|
+
if (!server && !code.includes(directives.server) && hasModuleDirective(code, clientDirective) && !excluded(relative)) {
|
|
465
|
+
const rewritten = rewriteFormStatusImports({
|
|
466
|
+
code,
|
|
467
|
+
id: file,
|
|
468
|
+
parse: parseFile
|
|
469
|
+
});
|
|
470
|
+
if (rewritten) return rewritten;
|
|
471
|
+
}
|
|
463
472
|
const actions = transformActions({
|
|
464
473
|
code,
|
|
465
474
|
id: file,
|
|
@@ -12,12 +12,22 @@
|
|
|
12
12
|
declare const ISLAND_ELEMENT = "f-island";
|
|
13
13
|
/** The script type of an island's props; its `for` attribute names the island. */
|
|
14
14
|
declare const PROPS_SCRIPT_TYPE = "f/props";
|
|
15
|
+
/**
|
|
16
|
+
* On a props script: the id React gave the island's position on the server, so the browser
|
|
17
|
+
* hydrates the island where the server rendered it and `useId` matches.
|
|
18
|
+
*/
|
|
19
|
+
declare const ID_POSITION_ATTRIBUTE = "ids";
|
|
15
20
|
/** The script type of a value sent once; its `name` attribute is the value's key. */
|
|
16
21
|
declare const VALUE_SCRIPT_TYPE = "f/value";
|
|
17
22
|
/** Set on an island once it hydrated (and its held interaction was replayed). */
|
|
18
23
|
declare const HYDRATED_ATTRIBUTE = "hydrated";
|
|
19
24
|
/** The element that keeps server output passed to an island as children. */
|
|
20
25
|
declare const SLOT_ELEMENT = "f-slot";
|
|
26
|
+
/**
|
|
27
|
+
* After an island's props: `<template for>` holding the `<f-slot>`s the island did not place when
|
|
28
|
+
* the server rendered it, which the browser shows when the island places them.
|
|
29
|
+
*/
|
|
30
|
+
declare const SLOT_TEMPLATE_ELEMENT = "template";
|
|
21
31
|
/** The script type of a page's server tree, rebuilt in the browser without the page's HTML. */
|
|
22
32
|
declare const TREE_SCRIPT_TYPE = "f/tree";
|
|
23
33
|
/** The script type of a server tree chunk that streamed in after the tree. */
|
|
@@ -41,6 +51,11 @@ declare const TAG_BOUND = "b";
|
|
|
41
51
|
declare const REFERENCE_TAG = "r";
|
|
42
52
|
/** `{"$":"s","v":name}`: a slot. */
|
|
43
53
|
declare const SLOT_TAG = "s";
|
|
54
|
+
/**
|
|
55
|
+
* On a slot reference: `1` when the slot carries a server tree (`<script type="f/tree">` named
|
|
56
|
+
* after the slot) that the browser rebuilds inside the island, instead of keeping its HTML.
|
|
57
|
+
*/
|
|
58
|
+
declare const SLOT_TREE_FIELD = "t";
|
|
44
59
|
/** `{"$":"a","v":id}` or `{"$":"a","v":id,"b":sealed}`: a server action. */
|
|
45
60
|
declare const ACTION_TAG = "a";
|
|
46
61
|
/**
|
|
@@ -105,4 +120,4 @@ declare const MAX_HELD_VALUES = 128;
|
|
|
105
120
|
* `{"$":"a","v":id,"b":sealed}`), or undefined for anything else.
|
|
106
121
|
*/
|
|
107
122
|
declare function getTag(value: unknown): string | undefined;
|
|
108
|
-
export {
|
|
123
|
+
export { TAG_BOUND as A, stripRuntimeFields as B, REFERENCE_TAG as C, SLOT_TEMPLATE_ELEMENT as D, SLOT_TAG as E, VALUE_SCRIPT_TYPE as F, attachmentField as I, formFieldName as L, TREE_SCRIPT_TYPE as M, VALUES_HEADER as N, SLOT_TREE_FIELD as O, VALUE_KEY_PATTERN as P, getTag as R, PROPS_SCRIPT_TYPE as S, SLOT_ELEMENT as T, LAYOUTS_HEADER as _, BLOB_TAG as a, PROMISE_SCRIPT_TYPE as b, CHUNK_SCRIPT_TYPE as c, FORM_REF_PREFIX as d, FORM_STATE_SCRIPT_TYPE as f, ISLAND_ELEMENT as g, ID_POSITION_ATTRIBUTE as h, ACTION_TAG as i, TAG_VALUE as j, TAG as k, FORM_DATA_TAG as l, HYDRATED_ATTRIBUTE as m, ACTION_KEY_FIELD as n, BOUND_FIELD as o, HTML_ELEMENT as p, ACTION_STALE_HEADER as r, CALL_FIELD as s, ACTION_HEADER as t, FORM_FIELD_PREFIX as u, MAX_HELD_VALUES as v, RENDER_HEADER as w, PROMISE_TAG as x, OBJECT_TAG as y, readPostedAction as z };
|
|
@@ -13,12 +13,22 @@
|
|
|
13
13
|
const ISLAND_ELEMENT = "f-island";
|
|
14
14
|
/** The script type of an island's props; its `for` attribute names the island. */
|
|
15
15
|
const PROPS_SCRIPT_TYPE = "f/props";
|
|
16
|
+
/**
|
|
17
|
+
* On a props script: the id React gave the island's position on the server, so the browser
|
|
18
|
+
* hydrates the island where the server rendered it and `useId` matches.
|
|
19
|
+
*/
|
|
20
|
+
const ID_POSITION_ATTRIBUTE = "ids";
|
|
16
21
|
/** The script type of a value sent once; its `name` attribute is the value's key. */
|
|
17
22
|
const VALUE_SCRIPT_TYPE = "f/value";
|
|
18
23
|
/** Set on an island once it hydrated (and its held interaction was replayed). */
|
|
19
24
|
const HYDRATED_ATTRIBUTE = "hydrated";
|
|
20
25
|
/** The element that keeps server output passed to an island as children. */
|
|
21
26
|
const SLOT_ELEMENT = "f-slot";
|
|
27
|
+
/**
|
|
28
|
+
* After an island's props: `<template for>` holding the `<f-slot>`s the island did not place when
|
|
29
|
+
* the server rendered it, which the browser shows when the island places them.
|
|
30
|
+
*/
|
|
31
|
+
const SLOT_TEMPLATE_ELEMENT = "template";
|
|
22
32
|
/** The script type of a page's server tree, rebuilt in the browser without the page's HTML. */
|
|
23
33
|
const TREE_SCRIPT_TYPE = "f/tree";
|
|
24
34
|
/** The script type of a server tree chunk that streamed in after the tree. */
|
|
@@ -42,6 +52,11 @@ const TAG_BOUND = "b";
|
|
|
42
52
|
const REFERENCE_TAG = "r";
|
|
43
53
|
/** `{"$":"s","v":name}`: a slot. */
|
|
44
54
|
const SLOT_TAG = "s";
|
|
55
|
+
/**
|
|
56
|
+
* On a slot reference: `1` when the slot carries a server tree (`<script type="f/tree">` named
|
|
57
|
+
* after the slot) that the browser rebuilds inside the island, instead of keeping its HTML.
|
|
58
|
+
*/
|
|
59
|
+
const SLOT_TREE_FIELD = "t";
|
|
45
60
|
/** `{"$":"a","v":id}` or `{"$":"a","v":id,"b":sealed}`: a server action. */
|
|
46
61
|
const ACTION_TAG = "a";
|
|
47
62
|
/**
|
|
@@ -127,5 +142,6 @@ function getTag(value) {
|
|
|
127
142
|
if (keys.length === 1) return tag;
|
|
128
143
|
if (keys.length === 2 && "v" in value) return tag;
|
|
129
144
|
if (keys.length === 3 && tag === "a" && "v" in value && "b" in value) return tag;
|
|
145
|
+
if (keys.length === 3 && tag === "s" && "v" in value && "t" in value) return tag;
|
|
130
146
|
}
|
|
131
|
-
export {
|
|
147
|
+
export { TAG_BOUND as A, stripRuntimeFields as B, REFERENCE_TAG as C, SLOT_TEMPLATE_ELEMENT as D, SLOT_TAG as E, VALUE_SCRIPT_TYPE as F, attachmentField as I, formFieldName as L, TREE_SCRIPT_TYPE as M, VALUES_HEADER as N, SLOT_TREE_FIELD as O, VALUE_KEY_PATTERN as P, getTag as R, PROPS_SCRIPT_TYPE as S, SLOT_ELEMENT as T, LAYOUTS_HEADER as _, BLOB_TAG as a, PROMISE_SCRIPT_TYPE as b, CHUNK_SCRIPT_TYPE as c, FORM_REF_PREFIX as d, FORM_STATE_SCRIPT_TYPE as f, ISLAND_ELEMENT as g, ID_POSITION_ATTRIBUTE as h, ACTION_TAG as i, TAG_VALUE as j, TAG as k, FORM_DATA_TAG as l, HYDRATED_ATTRIBUTE as m, ACTION_KEY_FIELD as n, BOUND_FIELD as o, HTML_ELEMENT as p, ACTION_STALE_HEADER as r, CALL_FIELD as s, ACTION_HEADER as t, FORM_FIELD_PREFIX as u, MAX_HELD_VALUES as v, RENDER_HEADER as w, PROMISE_TAG as x, OBJECT_TAG as y, readPostedAction as z };
|