@farm.js/islx 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.3
4
+
5
+ Server layouts as trees.
6
+
7
+ - A server layout around a client provider renders as a server tree, so a framework no longer
8
+ ships the layout's module to hydrate it. On the server, `renderServerTree(layout, { outlets,
9
+ scope })` with `<Outlet />` where the page goes; in the browser, `createServerTreeLayout(source,
10
+ scope, { load, ServerHtml, onUpdate })` and `layout.render({ children: page })`. Rendering it
11
+ again with another page keeps the provider's state.
12
+ - A layout's tree names its payload, chunks and server HTML by its scope (`f/chunk name="/:0"`),
13
+ so it and the page's tree at its outlet never read each other's parts. New tag `L` (outlet).
14
+
3
15
  ## 0.0.2
4
16
 
5
17
  Streaming and hydration fixes.
package/README.md CHANGED
@@ -13,7 +13,7 @@ islx started as [Farm.js](https://github.com/farming-labs/farm.js)'s islands mod
13
13
  replaces React Server Components' Flight payload. On a 300-page docs app it ships 26% less
14
14
  JavaScript, and a client navigation sends 2.5 KB instead of 12 KB.
15
15
 
16
- > Status: 0.0.2. React and Vite are supported; other renderers and framework
16
+ > Status: 0.0.3. React and Vite are supported; other renderers and framework
17
17
  > integrations follow. The wire format is in [SPEC.md](./SPEC.md), changes in
18
18
  > [CHANGELOG.md](./CHANGELOG.md).
19
19
 
@@ -180,6 +180,7 @@ A framework that renders pages itself (Farm does) uses the pieces directly inste
180
180
  | Render islands in its own render | wrap the page in `<IslandsProvider held={parseHeldValues(request)}>` |
181
181
  | A layout that hydrates as one root | `withinClientRoot(layout)`; its server-HTML page: `isolateIslands(page)` |
182
182
  | An async page under such a layout | `renderServerTree(page, { held, payloadOnly })` on the server; `createServerTreeElement(source, { load, ServerHtml })` in the browser |
183
+ | A server layout around a client provider, without shipping the layout | `renderServerTree(<Layout><Outlet /></Layout>, { outlets: { children: page }, scope })` on the server; `createServerTreeLayout(source, scope, { load, ServerHtml, onUpdate })` in the browser, then `layout.render({ children: page })` |
183
184
  | Hydrate | `createHydrationRuntime({ load, schedule?, wrap?, report? })`, `createServerHtml(runtime)` |
184
185
  | Hold early clicks | inline `PREHYDRATION_SCRIPT` in the page |
185
186
  | Actions | `handleAction(request, { allowedOrigins, bodySizeLimit, run, mapError, renderFormState })`; `setActionNavigator({ refresh, navigate })` |
package/SPEC.md CHANGED
@@ -32,9 +32,9 @@ Anything that writes and reads it, in any language or framework, is a islx imple
32
32
  | `<script type="f/props" for>` | An island's props, as JSON. `for` names the island's `props` id. |
33
33
  | `<script type="f/value" name>` | A value sent once. `name` is its key. Written before the first island that uses it. |
34
34
  | `<f-slot name>` | Server output passed to an island as children. It stays server HTML; the island renders it in place. |
35
- | `<script type="f/tree" [partial]>` | A page's server tree, so the browser can rebuild the page inside an existing root without its HTML. `partial` marks a navigation that sent only the tree. |
36
- | `<script type="f/chunk" name>` | A part of a server tree that streamed in after the tree (a resolved Suspense boundary). |
37
- | `<f-html name>` | Server HTML inside a server tree. |
35
+ | `<script type="f/tree" [name] [partial]>` | A server tree, so the browser can rebuild the page inside an existing root without its HTML. A page's tree has no `name`; a layout's carries its scope and sits at its `children` outlet, or after the layout when it has none. `partial` marks a navigation that sent only the tree. |
36
+ | `<script type="f/chunk" name>` | A part of a server tree that streamed in after the tree (a resolved Suspense boundary). `name` is its number, after `scope:` in a layout's tree. |
37
+ | `<f-html name>` | Server HTML inside a server tree, named like a chunk. |
38
38
 
39
39
  A value script's and props script's JSON escapes `<`, U+2028 and U+2029.
40
40
 
@@ -59,6 +59,7 @@ with any other keys is data.
59
59
  | `R` | `RegExp` | `[source, flags]` |
60
60
  | `Y` | bytes | `[type, base64]`; type is `ArrayBuffer`, `DataView` or a typed array name |
61
61
  | `p` | a Suspense boundary inside a server tree | |
62
+ | `L` | a layout tree's outlet, where the page renders | `n`: the outlet's name, `children` for the page |
62
63
  | `F` | a `FormData` action argument | its index among the call's attachments |
63
64
  | `K` | a `Blob` or `File` action argument | its index among the call's attachments |
64
65
 
@@ -58,6 +58,13 @@ declare function createIsland<P extends object>(Component: React.ComponentType<P
58
58
  * page in its layout's React root and never shows its HTML, so the server leaves the HTML out.
59
59
  */
60
60
  declare const PAYLOAD_ONLY_ATTRIBUTE = "partial";
61
+ /**
62
+ * Where a layout rendered as a server tree places the page (or another outlet, by `name`). The
63
+ * tree records the outlet, and the server and the browser render what they are given there.
64
+ */
65
+ declare function Outlet(_props: {
66
+ name?: string;
67
+ }): null;
61
68
  interface ServerTreeRenderOptions {
62
69
  /**
63
70
  * Renders a part the resolver cannot represent, as server HTML whose client components are
@@ -71,6 +78,17 @@ interface ServerTreeRenderOptions {
71
78
  * streams and the server HTML parts it cannot represent, without the page's rendered HTML.
72
79
  */
73
80
  payloadOnly?: boolean;
81
+ /**
82
+ * A layout rendered as a server tree: what each `<Outlet>` renders, by name. The payload script
83
+ * then sits at the `children` outlet, inside the layout's markup, so a layout that renders the
84
+ * whole document keeps it in the body.
85
+ */
86
+ outlets?: Record<string, React.ReactNode>;
87
+ /**
88
+ * Names this tree's payload, chunks and server HTML parts, so a layout's tree and the trees at
89
+ * its outlet do not read each other's. A page's tree has none.
90
+ */
91
+ scope?: string;
74
92
  }
75
93
  /**
76
94
  * Resolves a server page and returns what renders in its place: the resolved tree and its
@@ -99,4 +117,23 @@ interface ServerTreeElementOptions {
99
117
  * navigation), after loading its client modules. Returns null for a page without a tree.
100
118
  */
101
119
  declare function createServerTreeElement(source: ParentNode | string, options: ServerTreeElementOptions): Promise<React.ReactElement | null>;
102
- export { renderServerTree as a, IslandRenderState as c, getClientReference as d, isolateIslands as f, createServerTreeElement as i, IslandsProvider as l, withinClientRoot as m, ServerTreeElementOptions as n, updateServerTrees as o, setRenderStateResolver as p, ServerTreeRenderOptions as r, ClientReference as s, PAYLOAD_ONLY_ATTRIBUTE as t, createIsland as u };
120
+ /** A layout rebuilt from its server tree. */
121
+ interface ServerTreeLayout {
122
+ /** The layout around these outlets: plain elements, the same tree the server rendered. */
123
+ render(outlets: Record<string, React.ReactNode>): React.ReactNode;
124
+ /** Stops following hot module updates. */
125
+ dispose(): void;
126
+ }
127
+ interface ServerTreeLayoutOptions extends Omit<ServerTreeElementOptions, "key"> {
128
+ /** Runs after a hot update to one of the layout's client modules: render the route again. */
129
+ onUpdate?(): void;
130
+ }
131
+ /**
132
+ * Rebuilds a layout the server rendered with `renderServerTree(..., { outlets, scope })`, from
133
+ * its server HTML or a navigation's HTML. The layout's own module never reaches the browser: its
134
+ * client components load from the tree, and `render(outlets)` places the page at its outlet.
135
+ * Rendering it with the same component types at the same place keeps its client state across
136
+ * navigations. Returns null when `source` has no tree for `scope`.
137
+ */
138
+ declare function createServerTreeLayout(source: ParentNode | string, scope: string, options: ServerTreeLayoutOptions): Promise<ServerTreeLayout | null>;
139
+ export { setRenderStateResolver as _, ServerTreeLayoutOptions as a, createServerTreeLayout as c, ClientReference as d, IslandRenderState as f, isolateIslands as g, getClientReference as h, ServerTreeLayout as i, renderServerTree as l, createIsland as m, PAYLOAD_ONLY_ATTRIBUTE as n, ServerTreeRenderOptions as o, IslandsProvider as p, ServerTreeElementOptions as r, createServerTreeElement as s, Outlet as t, updateServerTrees as u, withinClientRoot as v };
@@ -228,6 +228,15 @@ const SERVER_HTML_TAG = HTML_ELEMENT;
228
228
  */
229
229
  const PAYLOAD_ONLY_ATTRIBUTE = "partial";
230
230
  const SERVER_HTML_ATTRIBUTE = "name";
231
+ const OUTLET = Symbol.for("islx.react.outlet");
232
+ /**
233
+ * Where a layout rendered as a server tree places the page (or another outlet, by `name`). The
234
+ * tree records the outlet, and the server and the browser render what they are given there.
235
+ */
236
+ function Outlet(_props) {
237
+ return null;
238
+ }
239
+ Outlet[OUTLET] = true;
231
240
  const PARSER_SENSITIVE_TAGS = /* @__PURE__ */ new Set([
232
241
  "caption",
233
242
  "colgroup",
@@ -414,6 +423,10 @@ async function resolveElement(state, element, context) {
414
423
  i: id
415
424
  });
416
425
  }
426
+ if (typeof type === "function" && type[OUTLET]) return withKey({
427
+ $: "L",
428
+ n: String(props.name ?? "children")
429
+ });
417
430
  const clientReference = getClientReference(type);
418
431
  if (clientReference) {
419
432
  const { children, ...rest } = props;
@@ -511,6 +524,7 @@ function createTreeValue(value, resolver) {
511
524
  fallback: createTreeValue(value.f, resolver)
512
525
  }, React.createElement(getChunkComponent(), { chunk: resolver.chunk(value.i) }));
513
526
  case "x": return resolver.serverHtml(value.i, typeof value.k === "string" ? value.k : null);
527
+ case "L": return resolver.outlet(value.n);
514
528
  case "n": return Number(value.v);
515
529
  case "a": return resolver.action(value.v, typeof value.b === "string" ? value.b : void 0);
516
530
  case "P": return fulfilledThenable(createTreeValue(value.v, resolver));
@@ -571,6 +585,22 @@ async function createPayloadOnlyPage(state, payload, encode, resolver) {
571
585
  return React.createElement(React.Fragment, { key: `c${id}` }, createTreeScript(CHUNK_SCRIPT_TYPE, String(id), chunk.json, chunk.values));
572
586
  }));
573
587
  }
588
+ /** The name of a tree part: its number, after the tree's scope. */
589
+ function scopedName(scope, id) {
590
+ return scope ? `${scope}:${id}` : String(id);
591
+ }
592
+ function hasChildrenOutlet(value) {
593
+ if (value === null || typeof value !== "object") return false;
594
+ if (Array.isArray(value)) return value.some(hasChildrenOutlet);
595
+ if (value.$ === "L") return value.n === "children";
596
+ return Object.values(value).some(hasChildrenOutlet);
597
+ }
598
+ /** What renders at a layout's outlet: its content, after the layout's payload at `children`. */
599
+ function outletContent(outlets, name, script) {
600
+ const content = outlets?.[name] ?? null;
601
+ if (name !== "children" || !script) return content;
602
+ return React.createElement(React.Fragment, null, script, content);
603
+ }
574
604
  /**
575
605
  * Resolves a server page and returns what renders in its place: the resolved tree and its
576
606
  * payload. Errors from the page's own code (a framework's redirect or not-found) propagate
@@ -588,6 +618,7 @@ async function renderServerTree(element, options = {}) {
588
618
  chunks: /* @__PURE__ */ new Map(),
589
619
  actions: /* @__PURE__ */ new Map()
590
620
  };
621
+ const scope = options.scope;
591
622
  const tree = await resolveValue(state, element, { parserSensitive: false }, /* @__PURE__ */ new Set());
592
623
  const held = options.held && options.held.size > 0 ? options.held : null;
593
624
  const encode = (payload) => {
@@ -597,6 +628,7 @@ async function renderServerTree(element, options = {}) {
597
628
  values: addressed.values.map((value) => [value.key, hook.escape(value.json)])
598
629
  };
599
630
  };
631
+ let layoutScript = null;
600
632
  const resolver = {
601
633
  component(reference, exportName) {
602
634
  const Component = state.components.get(`${reference}#${exportName}`);
@@ -610,7 +642,7 @@ async function renderServerTree(element, options = {}) {
610
642
  status: "pending",
611
643
  promise: entry.encoded.then((encoded) => {
612
644
  const payload = encode({ t: encoded });
613
- chunk.value = React.createElement(React.Fragment, null, createTreeValue(encoded, resolver), createTreeScript(CHUNK_SCRIPT_TYPE, String(id), payload.json, payload.values));
645
+ chunk.value = React.createElement(React.Fragment, null, createTreeValue(encoded, resolver), createTreeScript(CHUNK_SCRIPT_TYPE, scopedName(scope, id), payload.json, payload.values));
614
646
  chunk.status = "fulfilled";
615
647
  return chunk.value;
616
648
  }, (error) => {
@@ -626,10 +658,13 @@ async function renderServerTree(element, options = {}) {
626
658
  serverHtml(id, key) {
627
659
  return React.createElement(SERVER_HTML_TAG, {
628
660
  ...key === null ? {} : { key },
629
- [SERVER_HTML_ATTRIBUTE]: String(id),
661
+ [SERVER_HTML_ATTRIBUTE]: scopedName(scope, id),
630
662
  style: { display: "contents" }
631
663
  }, isolate(state.serverHtml.get(id)));
632
664
  },
665
+ outlet(name) {
666
+ return outletContent(options.outlets, name, layoutScript);
667
+ },
633
668
  action(id) {
634
669
  return state.actions.get(id);
635
670
  }
@@ -638,6 +673,16 @@ async function renderServerTree(element, options = {}) {
638
673
  v: 1,
639
674
  t: tree
640
675
  });
676
+ if (options.outlets) {
677
+ const script = createTreeScript(TREE_SCRIPT_TYPE, scope ?? "", payload.json, payload.values);
678
+ const atOutlet = hasChildrenOutlet(tree);
679
+ if (atOutlet) layoutScript = script;
680
+ const layout = createTreeValue(tree, resolver);
681
+ const content = atOutlet ? layout : React.createElement(React.Fragment, null, layout, script);
682
+ return React.createElement(function ServerTreeLayout() {
683
+ return content;
684
+ });
685
+ }
641
686
  const page = options.payloadOnly && await createPayloadOnlyPage(state, payload, encode, resolver) || createTreePage(tree, payload.json, resolver, void 0, payload.values);
642
687
  return React.createElement(function ServerTreePage() {
643
688
  return page;
@@ -673,14 +718,10 @@ function exportOf(module, src, exportName) {
673
718
  if (typeof Component !== "function" && (typeof Component !== "object" || Component === null)) throw new Error(`${src} has no export ${exportName}`);
674
719
  return Component;
675
720
  }
676
- /**
677
- * Rebuilds a server-tree page in the browser from its server HTML (an element, or the HTML of a
678
- * navigation), after loading its client modules. Returns null for a page without a tree.
679
- */
680
- async function createServerTreeElement(source, options) {
721
+ /** Reads a tree's payload, chunks and server HTML from `root` and loads its client modules. */
722
+ async function prepareTree(root, scope, options) {
681
723
  const values = options.values ?? getValueStore();
682
- const root = typeof source === "string" ? parseHtml(source) : source;
683
- const script = root.querySelector(`script[type="${TREE_SCRIPT_TYPE}"]`);
724
+ const script = Array.from(root.querySelectorAll(`script[type="${TREE_SCRIPT_TYPE}"]`)).find((candidate) => (candidate.getAttribute("name") ?? void 0) === scope);
684
725
  if (!script) return null;
685
726
  const json = script.textContent ?? "";
686
727
  let ingested = false;
@@ -696,10 +737,19 @@ async function createServerTreeElement(source, options) {
696
737
  const payload = parsePayload(json);
697
738
  if (payload.v !== 1) throw new Error("unsupported server tree payload version");
698
739
  const scripts = readTreeValueScripts(script);
740
+ const partId = (name) => {
741
+ if (name === null) return null;
742
+ const prefix = scope === void 0 ? "" : `${scope}:`;
743
+ if (!name.startsWith(prefix)) return null;
744
+ const id = name.slice(prefix.length);
745
+ return /^\d+$/.test(id) ? Number(id) : null;
746
+ };
699
747
  const chunks = /* @__PURE__ */ new Map();
700
748
  for (const chunkScript of root.querySelectorAll(`script[type="${CHUNK_SCRIPT_TYPE}"]`)) {
749
+ const id = partId(chunkScript.getAttribute("name"));
750
+ if (id === null) continue;
701
751
  const chunkJson = chunkScript.textContent ?? "";
702
- chunks.set(Number(chunkScript.getAttribute("name")), {
752
+ chunks.set(id, {
703
753
  tree: parsePayload(chunkJson).t,
704
754
  json: chunkJson,
705
755
  values: readTreeValueScripts(chunkScript)
@@ -715,52 +765,84 @@ async function createServerTreeElement(source, options) {
715
765
  components.set(key, exportOf(await options.load(src), src, key.slice(separator + 1)));
716
766
  }));
717
767
  const serverHtml = /* @__PURE__ */ new Map();
718
- for (const container of root.querySelectorAll(`${SERVER_HTML_TAG}[${SERVER_HTML_ATTRIBUTE}]`)) serverHtml.set(Number(container.getAttribute(SERVER_HTML_ATTRIBUTE)), container.innerHTML);
719
- const resolver = {
720
- component(reference, exportName) {
721
- const Component = components.get(`${reference}#${exportName}`);
722
- if (!Component) throw new Error(`client reference ${reference}#${exportName} was not loaded`);
723
- return Component;
724
- },
725
- chunk(id) {
726
- const chunk = chunks.get(id);
727
- if (!chunk) throw new Error(`server tree chunk ${id} was not found`);
728
- const value = React.createElement(React.Fragment, null, createTreeValue(chunk.tree, resolver), createTreeScript(CHUNK_SCRIPT_TYPE, String(id), chunk.json, chunk.values));
729
- return {
730
- status: "fulfilled",
731
- value,
732
- promise: Promise.resolve(value)
733
- };
734
- },
735
- serverHtml(id, key) {
736
- const html = serverHtml.get(id);
737
- if (html === void 0) throw new Error(`server HTML ${id} was not found`);
738
- return React.createElement(options.ServerHtml, {
739
- ...key === null ? {} : { key },
740
- as: SERVER_HTML_TAG,
741
- [SERVER_HTML_ATTRIBUTE]: String(id),
742
- style: { display: "contents" },
743
- suppressHydrationWarning: true,
744
- dangerouslySetInnerHTML: { __html: html }
745
- });
746
- },
747
- action(id, sealed) {
748
- return createServerReference(id, sealed);
749
- }
768
+ for (const container of root.querySelectorAll(`${SERVER_HTML_TAG}[${SERVER_HTML_ATTRIBUTE}]`)) {
769
+ const id = partId(container.getAttribute(SERVER_HTML_ATTRIBUTE));
770
+ if (id !== null) serverHtml.set(id, container.innerHTML);
771
+ }
772
+ const resolver = (outlets, outletScript) => {
773
+ const treeResolver = {
774
+ component(reference, exportName) {
775
+ const Component = components.get(`${reference}#${exportName}`);
776
+ if (!Component) throw new Error(`client reference ${reference}#${exportName} was not loaded`);
777
+ return Component;
778
+ },
779
+ chunk(id) {
780
+ const chunk = chunks.get(id);
781
+ if (!chunk) throw new Error(`server tree chunk ${id} was not found`);
782
+ const value = React.createElement(React.Fragment, null, createTreeValue(chunk.tree, treeResolver), createTreeScript(CHUNK_SCRIPT_TYPE, scopedName(scope, id), chunk.json, chunk.values));
783
+ return {
784
+ status: "fulfilled",
785
+ value,
786
+ promise: Promise.resolve(value)
787
+ };
788
+ },
789
+ serverHtml(id, key) {
790
+ const html = serverHtml.get(id);
791
+ if (html === void 0) throw new Error(`server HTML ${id} was not found`);
792
+ return React.createElement(options.ServerHtml, {
793
+ ...key === null ? {} : { key },
794
+ as: SERVER_HTML_TAG,
795
+ [SERVER_HTML_ATTRIBUTE]: scopedName(scope, id),
796
+ style: { display: "contents" },
797
+ suppressHydrationWarning: true,
798
+ dangerouslySetInnerHTML: { __html: html }
799
+ });
800
+ },
801
+ outlet(name) {
802
+ return outletContent(outlets, name, outletScript);
803
+ },
804
+ action(id, sealed) {
805
+ return createServerReference(id, sealed);
806
+ }
807
+ };
808
+ return treeResolver;
750
809
  };
751
- const build = () => createTreePage(payload.t, json, resolver, void 0, scripts);
810
+ return {
811
+ payload: payload.t,
812
+ json,
813
+ values: scripts,
814
+ components,
815
+ resolver,
816
+ hasOutlet: hasChildrenOutlet(payload.t)
817
+ };
818
+ }
819
+ /** Points a tree's loaded components at a hot-updated module; true when the tree renders it. */
820
+ function updateTreeComponents(components, src, module) {
821
+ let changed = false;
822
+ for (const key of components.keys()) {
823
+ const separator = key.lastIndexOf("#");
824
+ if (key.slice(0, separator) !== src) continue;
825
+ components.set(key, exportOf(module, src, key.slice(separator + 1)));
826
+ changed = true;
827
+ }
828
+ return changed;
829
+ }
830
+ /**
831
+ * Rebuilds a server-tree page in the browser from its server HTML (an element, or the HTML of a
832
+ * navigation), after loading its client modules. Returns null for a page without a tree.
833
+ */
834
+ async function createServerTreeElement(source, options) {
835
+ const prepared = await prepareTree(typeof source === "string" ? parseHtml(source) : source, void 0, options);
836
+ if (!prepared) return null;
837
+ const tree = prepared;
838
+ const resolver = tree.resolver();
839
+ const build = () => createTreePage(tree.payload, tree.json, resolver, void 0, tree.values);
752
840
  build();
753
841
  function ServerTreePage() {
754
842
  const [, setVersion] = React.useState(0);
755
843
  React.useEffect(() => {
756
844
  const listener = (src, module) => {
757
- let changed = false;
758
- for (const key of components.keys()) {
759
- const separator = key.lastIndexOf("#");
760
- if (key.slice(0, separator) !== src) continue;
761
- components.set(key, exportOf(module, src, key.slice(separator + 1)));
762
- changed = true;
763
- }
845
+ const changed = updateTreeComponents(tree.components, src, module);
764
846
  if (changed) setVersion((version) => version + 1);
765
847
  return changed;
766
848
  };
@@ -774,4 +856,35 @@ async function createServerTreeElement(source, options) {
774
856
  }
775
857
  return React.createElement(ServerTreePage, options.key === void 0 ? null : { key: options.key });
776
858
  }
777
- export { IslandsProvider as a, getContexts as c, setIslandServerHook as d, setRenderStateResolver as f, updateServerTrees as i, isolateIslands as l, createServerTreeElement as n, createIsland as o, withinClientRoot as p, renderServerTree as r, getClientReference as s, PAYLOAD_ONLY_ATTRIBUTE as t, runtimeTagged as u };
859
+ /**
860
+ * Rebuilds a layout the server rendered with `renderServerTree(..., { outlets, scope })`, from
861
+ * its server HTML or a navigation's HTML. The layout's own module never reaches the browser: its
862
+ * client components load from the tree, and `render(outlets)` places the page at its outlet.
863
+ * Rendering it with the same component types at the same place keeps its client state across
864
+ * navigations. Returns null when `source` has no tree for `scope`.
865
+ */
866
+ async function createServerTreeLayout(source, scope, options) {
867
+ const tree = await prepareTree(typeof source === "string" ? parseHtml(source) : source, scope, options);
868
+ if (!tree) return null;
869
+ const script = createTreeScript(TREE_SCRIPT_TYPE, scope, tree.json, tree.values);
870
+ const render = (outlets) => {
871
+ const resolver = tree.resolver(outlets, tree.hasOutlet ? script : void 0);
872
+ const layout = createTreeValue(tree.payload, resolver);
873
+ return tree.hasOutlet ? layout : React.createElement(React.Fragment, null, layout, script);
874
+ };
875
+ render({});
876
+ const listener = (src, module) => {
877
+ const changed = updateTreeComponents(tree.components, src, module);
878
+ if (changed) options.onUpdate?.();
879
+ return changed;
880
+ };
881
+ const listeners = treeListeners();
882
+ listeners.add(listener);
883
+ return {
884
+ render,
885
+ dispose: () => {
886
+ listeners.delete(listener);
887
+ }
888
+ };
889
+ }
890
+ export { renderServerTree as a, createIsland as c, isolateIslands as d, runtimeTagged as f, withinClientRoot as h, createServerTreeLayout as i, getClientReference as l, setRenderStateResolver as m, PAYLOAD_ONLY_ATTRIBUTE as n, updateServerTrees as o, setIslandServerHook as p, createServerTreeElement as r, IslandsProvider as s, Outlet as t, getContexts as u };
@@ -1,7 +1,7 @@
1
1
  import { a as scheduleHydration, i as isIslandStrategy, n as IslandStrategy, r as ScheduleOptions, t as ISLAND_STRATEGIES } from "../_chunks/schedule-Dw14dW0H.mjs";
2
2
  import { m as HYDRATED_ATTRIBUTE } from "../_chunks/wire-C3Rpbwl6.mjs";
3
3
  import { t as ValueStore } from "../_chunks/value-store-Dm-7xJ6W.mjs";
4
- import { f as isolateIslands, i as createServerTreeElement, m as withinClientRoot, n as ServerTreeElementOptions, o as updateServerTrees, t as PAYLOAD_ONLY_ATTRIBUTE } from "../_chunks/tree-fID6sz25.mjs";
4
+ import { a as ServerTreeLayoutOptions, c as createServerTreeLayout, g as isolateIslands, i as ServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, r as ServerTreeElementOptions, s as createServerTreeElement, u as updateServerTrees, v as withinClientRoot } from "../_chunks/tree-B4RMU2UX.mjs";
5
5
  import * as React from "react";
6
6
 
7
7
  // src/react/hydrate.d.ts
@@ -46,4 +46,4 @@ declare function createServerHtml(runtime: Pick<HydrationRuntime, "hydrate" | "d
46
46
  declare function refreshDocument(runtime: HydrationRuntime): Promise<void>;
47
47
  /** Puts another page's body in place of this one's, and hydrates its islands. */
48
48
  declare function swapDocument(runtime: HydrationRuntime, next: Document): Promise<void>;
49
- export { HYDRATED_ATTRIBUTE, type HydrationOptions, type HydrationRuntime, ISLAND_STRATEGIES, type IslandStrategy, PAYLOAD_ONLY_ATTRIBUTE, type ScheduleOptions, type ServerTreeElementOptions, createHydrationRuntime, createServerHtml, createServerTreeElement, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
49
+ export { HYDRATED_ATTRIBUTE, type HydrationOptions, type HydrationRuntime, ISLAND_STRATEGIES, type IslandStrategy, PAYLOAD_ONLY_ATTRIBUTE, type ScheduleOptions, type ServerTreeElementOptions, type ServerTreeLayout, type ServerTreeLayoutOptions, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
@@ -1,7 +1,7 @@
1
1
  import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-2sTj0A2T.mjs";
2
2
  import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-DqfVYKsx.mjs";
3
3
  import { N as getTag, S as SLOT_ELEMENT, f as FORM_STATE_SCRIPT_TYPE, h as ISLAND_ELEMENT, m as HYDRATED_ATTRIBUTE, y as PROPS_SCRIPT_TYPE } from "../_chunks/wire-CXOodE06.mjs";
4
- import { c as getContexts, i as updateServerTrees, l as isolateIslands, n as createServerTreeElement, p as withinClientRoot, t as PAYLOAD_ONLY_ATTRIBUTE } from "../_chunks/tree-BDB5VEao.mjs";
4
+ import { d as isolateIslands, h as withinClientRoot, i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement, u as getContexts } from "../_chunks/tree-BvYR6-PF.mjs";
5
5
  import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-BFoGKRrU.mjs";
6
6
  import * as React from "react";
7
7
  import { hydrateRoot } from "react-dom/client";
@@ -453,4 +453,4 @@ async function swapDocument(runtime, next) {
453
453
  document.body.replaceChildren(...Array.from(body.childNodes));
454
454
  await runtime.hydrate(document);
455
455
  }
456
- export { HYDRATED_ATTRIBUTE, ISLAND_STRATEGIES, PAYLOAD_ONLY_ATTRIBUTE, createHydrationRuntime, createServerHtml, createServerTreeElement, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
456
+ export { HYDRATED_ATTRIBUTE, ISLAND_STRATEGIES, PAYLOAD_ONLY_ATTRIBUTE, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
@@ -1,5 +1,5 @@
1
1
  import { r as ReactFormState } from "../_chunks/handle-C0N9ZyNV.mjs";
2
- import { a as renderServerTree, c as IslandRenderState, d as getClientReference, f as isolateIslands, l as IslandsProvider, m as withinClientRoot, p as setRenderStateResolver, r as ServerTreeRenderOptions, s as ClientReference, t as PAYLOAD_ONLY_ATTRIBUTE, u as createIsland } from "../_chunks/tree-fID6sz25.mjs";
2
+ import { _ as setRenderStateResolver, d as ClientReference, f as IslandRenderState, g as isolateIslands, h as getClientReference, l as renderServerTree, m as createIsland, n as PAYLOAD_ONLY_ATTRIBUTE, o as ServerTreeRenderOptions, p as IslandsProvider, t as Outlet, v as withinClientRoot } from "../_chunks/tree-B4RMU2UX.mjs";
3
3
  import * as React from "react";
4
4
  import { RenderToReadableStreamOptions } from "react-dom/server";
5
5
 
@@ -48,4 +48,4 @@ declare function renderToStream(element: React.ReactNode, {
48
48
  }>;
49
49
  /** Whether a response rendered for `request` leaves out values its browser holds. */
50
50
  declare function isPersonalized(request: Request): boolean;
51
- export { type ClientReference, type IslandRenderState, IslandsProvider, PAYLOAD_ONLY_ATTRIBUTE, RenderToStreamOptions, type ServerTreeRenderOptions, createIsland, getClientReference, isPersonalized, isolateIslands, renderServerTree, renderToStream, setRenderStateResolver, withinClientRoot };
51
+ export { type ClientReference, type IslandRenderState, IslandsProvider, Outlet, PAYLOAD_ONLY_ATTRIBUTE, RenderToStreamOptions, type ServerTreeRenderOptions, createIsland, getClientReference, isPersonalized, isolateIslands, renderServerTree, renderToStream, setRenderStateResolver, withinClientRoot };
@@ -3,7 +3,7 @@ import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-CXOodE06.mjs";
3
3
  import { r as PREHYDRATION_SCRIPT } from "../_chunks/prehydration-B1GLTLdk.mjs";
4
4
  import { g as escapeScriptJson, h as addressValues, i as encodeActionReference, v as parseHeldValues, y as selectValuesToSend } from "../_chunks/actions-B8dJ8f7L.mjs";
5
5
  import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
6
- import { a as IslandsProvider, c as getContexts, d as setIslandServerHook, f as setRenderStateResolver, l as isolateIslands, o as createIsland, p as withinClientRoot, r as renderServerTree, s as getClientReference, t as PAYLOAD_ONLY_ATTRIBUTE, u as runtimeTagged } from "../_chunks/tree-BDB5VEao.mjs";
6
+ import { a as renderServerTree, c as createIsland, d as isolateIslands, f as runtimeTagged, h as withinClientRoot, l as getClientReference, m as setRenderStateResolver, n as PAYLOAD_ONLY_ATTRIBUTE, p as setIslandServerHook, s as IslandsProvider, t as Outlet, u as getContexts } from "../_chunks/tree-BvYR6-PF.mjs";
7
7
  import * as React from "react";
8
8
  // src/react/server.ts
9
9
  /**
@@ -81,4 +81,4 @@ async function renderToStream(element, { request, formState, bootstrap = true, p
81
81
  function isPersonalized(request) {
82
82
  return parseHeldValues(request).size > 0;
83
83
  }
84
- export { IslandsProvider, PAYLOAD_ONLY_ATTRIBUTE, createIsland, getClientReference, isPersonalized, isolateIslands, renderServerTree, renderToStream, setRenderStateResolver, withinClientRoot };
84
+ export { IslandsProvider, Outlet, PAYLOAD_ONLY_ATTRIBUTE, createIsland, getClientReference, isPersonalized, isolateIslands, renderServerTree, renderToStream, setRenderStateResolver, withinClientRoot };
package/dist/react.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { a as renderServerTree, c as IslandRenderState, f as isolateIslands, l as IslandsProvider, m as withinClientRoot, r as ServerTreeRenderOptions, u as createIsland } from "./_chunks/tree-fID6sz25.mjs";
1
+ import { f as IslandRenderState, g as isolateIslands, l as renderServerTree, m as createIsland, o as ServerTreeRenderOptions, p as IslandsProvider, t as Outlet, v as withinClientRoot } from "./_chunks/tree-B4RMU2UX.mjs";
2
2
  import { n as IslandStrategy } from "./_chunks/schedule-Dw14dW0H.mjs";
3
- export { type IslandRenderState, type IslandStrategy, IslandsProvider, type ServerTreeRenderOptions, createIsland, isolateIslands, renderServerTree, withinClientRoot };
3
+ export { type IslandRenderState, type IslandStrategy, IslandsProvider, Outlet, type ServerTreeRenderOptions, createIsland, isolateIslands, renderServerTree, withinClientRoot };
package/dist/react.mjs CHANGED
@@ -1 +1 @@
1
- export { a as IslandsProvider, o as createIsland, l as isolateIslands, r as renderServerTree, p as withinClientRoot } from "./_chunks/tree-BDB5VEao.mjs";
1
+ export { s as IslandsProvider, t as Outlet, c as createIsland, d as isolateIslands, a as renderServerTree, h as withinClientRoot } from "./_chunks/tree-BvYR6-PF.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/islx",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A renderer-agnostic wire format for server-rendered islands: typed props, values sent once, server actions, and partial navigations.",
5
5
  "keywords": [
6
6
  "islands",