@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +56 -26
  3. package/SPEC.md +5 -4
  4. package/dist/_chunks/{actions-C-IwUCKU.mjs → actions-DAzd4PpV.mjs} +2 -2
  5. package/dist/_chunks/{actions-CvQ0-zjg.mjs → actions-DzIPDecd.mjs} +2 -2
  6. package/dist/_chunks/{codec-C8to-RQW.mjs → codec-DDpUvgRo.mjs} +1 -1
  7. package/dist/_chunks/{actions-Eutk6UzT.mjs → form-status-CRfnB7WT.mjs} +314 -1
  8. package/dist/_chunks/forms-CCDY9QHC.mjs +63 -0
  9. package/dist/_chunks/{tree-C47p6khN.mjs → island-viWIOM1k.mjs} +501 -8
  10. package/dist/_chunks/{prehydration-B1GLTLdk.mjs → prehydration-zEYsfFxW.mjs} +3 -2
  11. package/dist/_chunks/{schedule-CjoybS3b.mjs → schedule-IK9_RwZC.mjs} +13 -4
  12. package/dist/_chunks/{tree-B4RMU2UX.d.mts → tree-CV13RsJv.d.mts} +8 -1
  13. package/dist/_chunks/{values-37UIzxM0.mjs → values-gxhIei_m.mjs} +2 -2
  14. package/dist/_chunks/{vite-BTzYlEiY.mjs → vite-CHB6B8tP.mjs} +36 -12
  15. package/dist/_chunks/{wire-Dipqh-EB.d.mts → wire-1d6wqOia.d.mts} +16 -1
  16. package/dist/_chunks/{wire-QEQWBr5s.mjs → wire-UUwZlUJZ.mjs} +17 -1
  17. package/dist/boot.d.mts +31 -0
  18. package/dist/boot.mjs +177 -0
  19. package/dist/client.d.mts +2 -1
  20. package/dist/client.mjs +4 -28
  21. package/dist/compiler.d.mts +35 -1
  22. package/dist/compiler.mjs +1 -1
  23. package/dist/index.d.mts +2 -2
  24. package/dist/index.mjs +3 -3
  25. package/dist/react/client.d.mts +2 -2
  26. package/dist/react/client.mjs +88 -19
  27. package/dist/react/server.d.mts +1 -1
  28. package/dist/react/server.mjs +13 -10
  29. package/dist/react.d.mts +6 -2
  30. package/dist/react.mjs +22 -3
  31. package/dist/server.d.mts +5 -0
  32. package/dist/server.mjs +4 -4
  33. package/dist/tanstack-start/server.mjs +3 -3
  34. package/dist/tanstack-start.mjs +1 -1
  35. package/dist/vite.d.mts +16 -1
  36. package/dist/vite.mjs +1 -1
  37. package/package.json +5 -1
  38. package/dist/_chunks/island-DVOr-OUw.mjs +0 -262
@@ -1,8 +1,75 @@
1
- import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-C8to-RQW.mjs";
2
- import { M as VALUE_SCRIPT_TYPE, c as CHUNK_SCRIPT_TYPE, k as TREE_SCRIPT_TYPE, p as HTML_ELEMENT } from "./wire-QEQWBr5s.mjs";
3
- import { t as createServerReference, u as getValueStore } from "./actions-C-IwUCKU.mjs";
4
- import { a as getIslandServerHook, o as isolateIslands, r as getClientReference } from "./island-DVOr-OUw.mjs";
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
- export { renderServerTree as a, createServerTreeLayout as i, PAYLOAD_ONLY_ATTRIBUTE as n, updateServerTrees as o, createServerTreeElement as r, Outlet as t };
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 t=e.target&&e.target.closest&&e.target.closest(${JSON.stringify(REPLAYABLE_SELECTOR)});if(!t||t.closest("a[href]")||!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
+ 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-B1GLTLdk.mjs";
2
- import { m as HYDRATED_ATTRIBUTE } from "./wire-QEQWBr5s.mjs";
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.closest(REPLAYABLE) : null;
194
- if (!target || !container.contains(target) || target.closest("a[href]") || mouse.button !== 0 || mouse.metaKey || mouse.altKey || mouse.ctrlKey || mouse.shiftKey) return;
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, strategy?: IslandStrategy): React.ComponentType<P>;
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-C8to-RQW.mjs";
2
- import { A as VALUES_HEADER, j as VALUE_KEY_PATTERN } from "./wire-QEQWBr5s.mjs";
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