@farm.js/islx 0.0.4 → 0.0.5
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 +13 -0
- package/README.md +36 -1
- package/dist/_chunks/{actions-CtLpJPEB.mjs → actions-BuS5jDi0.mjs} +3 -144
- package/dist/_chunks/island-DVOr-OUw.mjs +262 -0
- package/dist/_chunks/{tree-opIpaCke.mjs → tree-BUkGP4AR.mjs} +3 -261
- package/dist/_chunks/values-37UIzxM0.mjs +144 -0
- package/dist/_chunks/vite-DG7hBn8A.mjs +503 -0
- package/dist/react/client.mjs +2 -1
- package/dist/react/server.mjs +4 -2
- package/dist/react.mjs +3 -1
- package/dist/server.mjs +2 -1
- package/dist/tanstack-start/server.d.mts +11 -0
- package/dist/tanstack-start/server.mjs +54 -0
- package/dist/tanstack-start.d.mts +8 -0
- package/dist/tanstack-start.mjs +88 -0
- package/dist/vite.d.mts +21 -2
- package/dist/vite.mjs +1 -497
- package/package.json +19 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.5
|
|
4
|
+
|
|
5
|
+
TanStack Start.
|
|
6
|
+
|
|
7
|
+
- `@farm.js/islx/tanstack-start`: `islxTanstackStart()` next to `tanstackStart()` runs a Start app
|
|
8
|
+
as islands, with configuration only. Routes, loaders and layouts render on the server; the
|
|
9
|
+
browser loads islx and the `"use client"` components a page uses (the modules under `src/`), not
|
|
10
|
+
the router, the route tree or loader data. Start's client entry becomes islx's and the router is
|
|
11
|
+
not dehydrated. Links move between routes without loading a document; a loader's promise passed
|
|
12
|
+
unawaited to a client component streams in; islands call Start server functions and islx server
|
|
13
|
+
actions. An app with its own `src/server.ts` uses `islandsHandler` from
|
|
14
|
+
`@farm.js/islx/tanstack-start/server`. Needs Node 22.12 or later (Start's own requirement).
|
|
15
|
+
|
|
3
16
|
## 0.0.4
|
|
4
17
|
|
|
5
18
|
Promise props, and islands in a page that streams after it loaded.
|
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.
|
|
16
|
+
> Status: 0.0.5. React, Vite and TanStack Start 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
|
|
|
@@ -169,6 +169,41 @@ export function Chart(props: { points: Map<string, number> }) { … }
|
|
|
169
169
|
`examples/react` is a runnable app; `pnpm test:e2e` runs it in Chrome, in development and from
|
|
170
170
|
its production build.
|
|
171
171
|
|
|
172
|
+
## TanStack Start
|
|
173
|
+
|
|
174
|
+
A Start app as islands, with configuration only:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// vite.config.ts
|
|
178
|
+
import { defineConfig } from "vite";
|
|
179
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
180
|
+
import react from "@vitejs/plugin-react";
|
|
181
|
+
import { islxTanstackStart } from "@farm.js/islx/tanstack-start";
|
|
182
|
+
|
|
183
|
+
export default defineConfig({ plugins: [tanstackStart(), react(), islxTanstackStart()] });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Routes, loaders and layouts render on the server. The browser loads islx and the `"use client"`
|
|
187
|
+
components a page uses (the modules under `src/`), not the router, the route tree or loader
|
|
188
|
+
data: Start's client entry becomes islx's, `<Scripts />` still loads it, and the router is not
|
|
189
|
+
dehydrated. Links move between routes without loading a document. A loader's promise passed
|
|
190
|
+
unawaited to a client component streams in. Islands call Start server functions
|
|
191
|
+
(`createServerFn`) and islx server actions (`"use server"`).
|
|
192
|
+
|
|
193
|
+
- An app with its own `src/server.ts` renders with islx's handler:
|
|
194
|
+
`createServerEntry({ fetch: createStartHandler(islandsHandler) })`, with `islandsHandler` from
|
|
195
|
+
`@farm.js/islx/tanstack-start/server` (and `handleAction` from `@farm.js/islx/server` in front of
|
|
196
|
+
it for islx actions).
|
|
197
|
+
- Client components run without a router in the browser: they cannot use `useRouter`,
|
|
198
|
+
`useNavigate` or `<Link>` (render plain `<a>` links; the server renders `<Link>` in routes).
|
|
199
|
+
- A library's `"use client"` components render as server HTML.
|
|
200
|
+
- A click before the islands hydrate is not replayed yet (the `interaction` strategy hydrates on
|
|
201
|
+
it, but the click itself is dropped).
|
|
202
|
+
- Start sends a bot the whole page before any of it; people get it streamed.
|
|
203
|
+
- Start needs Node 22.12 or later.
|
|
204
|
+
|
|
205
|
+
`examples/tanstack-start` runs in the e2e suite, in development and from its production build.
|
|
206
|
+
|
|
172
207
|
## In a framework
|
|
173
208
|
|
|
174
209
|
A framework that renders pages itself (Farm does) uses the pieces directly instead of
|
|
@@ -1,147 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as encode, c as needsObjectTag, o as encodeLeaf, r as decode, t as EncodeError } from "./codec-C8to-RQW.mjs";
|
|
1
|
+
import { P as formFieldName, d as FORM_REF_PREFIX } from "./wire-QEQWBr5s.mjs";
|
|
3
2
|
import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Values sent once: which parts of an island's props travel as values of their own, keyed by a
|
|
7
|
-
* hash of their JSON, and which of those a response leaves out because the browser already holds
|
|
8
|
-
* them. The browser side is `@farm.js/islx/client` (`getValueStore`).
|
|
9
|
-
*/
|
|
10
|
-
/** Shorter values stay inline: a reference and its script cost about 60 bytes. */
|
|
11
|
-
const VALUE_MIN_LENGTH = 512;
|
|
12
|
-
/** A 64-bit hash of a value's JSON, as 14 base-36 characters. Not cryptographic. */
|
|
13
|
-
function hashValue(json) {
|
|
14
|
-
let h1 = 3735928559 ^ json.length;
|
|
15
|
-
let h2 = 1103547991 ^ json.length;
|
|
16
|
-
for (let index = 0; index < json.length; index++) {
|
|
17
|
-
const code = json.charCodeAt(index);
|
|
18
|
-
h1 = Math.imul(h1 ^ code, 2654435761);
|
|
19
|
-
h2 = Math.imul(h2 ^ code, 1597334677);
|
|
20
|
-
}
|
|
21
|
-
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
22
|
-
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
23
|
-
return (h2 >>> 0).toString(36).padStart(7, "0") + (h1 >>> 0).toString(36).padStart(7, "0");
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Serializes props with every value whose JSON reaches `minLength` replaced by a reference to it.
|
|
27
|
-
* Types JSON cannot hold are tagged first (the codec), so a `Map` or a `Date` is addressed like
|
|
28
|
-
* any other value. Inner values are addressed first, so a large value that differs between pages
|
|
29
|
-
* in one place still shares its unchanged parts.
|
|
30
|
-
*/
|
|
31
|
-
function addressValues(props, { minLength = 512, encodeSpecial, encoded = false } = {}) {
|
|
32
|
-
const values = /* @__PURE__ */ new Map();
|
|
33
|
-
const reference = (json, children, parent) => {
|
|
34
|
-
const key = hashValue(json);
|
|
35
|
-
if (!values.has(key)) values.set(key, {
|
|
36
|
-
key,
|
|
37
|
-
json,
|
|
38
|
-
children
|
|
39
|
-
});
|
|
40
|
-
parent.push(key);
|
|
41
|
-
return `{"$":"r","v":"${key}"}`;
|
|
42
|
-
};
|
|
43
|
-
const seen = /* @__PURE__ */ new Set();
|
|
44
|
-
const encode = (value, parent) => {
|
|
45
|
-
const special = encoded ? void 0 : encodeSpecial?.(value);
|
|
46
|
-
if (special !== void 0) {
|
|
47
|
-
const json = JSON.stringify(special);
|
|
48
|
-
return [json, json.length];
|
|
49
|
-
}
|
|
50
|
-
const leaf = encoded ? void 0 : encodeLeaf(value);
|
|
51
|
-
if (leaf) {
|
|
52
|
-
const json = JSON.stringify(leaf);
|
|
53
|
-
return [json, json.length];
|
|
54
|
-
}
|
|
55
|
-
if (typeof value === "function") throw new TypeError("functions are not serializable");
|
|
56
|
-
if (typeof value === "symbol") throw new TypeError("symbol values are not serializable");
|
|
57
|
-
if (value === null || typeof value !== "object") {
|
|
58
|
-
const json = JSON.stringify(value);
|
|
59
|
-
return [typeof value === "string" && json.length >= minLength ? reference(json, [], parent) : json, json.length];
|
|
60
|
-
}
|
|
61
|
-
if (seen.has(value)) throw new TypeError("circular values are not serializable");
|
|
62
|
-
seen.add(value);
|
|
63
|
-
try {
|
|
64
|
-
if (!encoded && (value instanceof Map || value instanceof Set)) {
|
|
65
|
-
const entries = value instanceof Map ? Array.from(value, ([key, entry]) => [key, entry]) : [...value];
|
|
66
|
-
const [json, length] = encode(entries, parent);
|
|
67
|
-
const prefix = `{"$":"${value instanceof Map ? "M" : "S"}","v":`;
|
|
68
|
-
return [`${prefix}${json}}`, length + prefix.length + 1];
|
|
69
|
-
}
|
|
70
|
-
const prototype = Object.getPrototypeOf(value);
|
|
71
|
-
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) throw new TypeError("class instances are not serializable");
|
|
72
|
-
const own = [];
|
|
73
|
-
let length = 2;
|
|
74
|
-
const parts = [];
|
|
75
|
-
if (Array.isArray(value)) for (const entry of value) {
|
|
76
|
-
const [json, entryLength] = encode(entry, own);
|
|
77
|
-
parts.push(json);
|
|
78
|
-
length += entryLength + 1;
|
|
79
|
-
}
|
|
80
|
-
else for (const [key, entry] of Object.entries(value)) {
|
|
81
|
-
const [json, entryLength] = encode(entry, own);
|
|
82
|
-
const name = JSON.stringify(key);
|
|
83
|
-
parts.push(`${name}:${json}`);
|
|
84
|
-
length += name.length + entryLength + 2;
|
|
85
|
-
}
|
|
86
|
-
let json = Array.isArray(value) ? `[${parts.join(",")}]` : `{${parts.join(",")}}`;
|
|
87
|
-
if (!encoded && !Array.isArray(value) && needsObjectTag(value)) {
|
|
88
|
-
json = `{"$":"o","v":${json}}`;
|
|
89
|
-
length += 14;
|
|
90
|
-
}
|
|
91
|
-
const wrapper = own.length === 1 && json.length < minLength / 4;
|
|
92
|
-
if (length >= minLength && !wrapper) return [reference(json, own, parent), length];
|
|
93
|
-
parent.push(...own);
|
|
94
|
-
return [json, length];
|
|
95
|
-
} finally {
|
|
96
|
-
seen.delete(value);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
const roots = [];
|
|
100
|
-
return {
|
|
101
|
-
json: `{${Object.entries(props).map(([key, entry]) => `${JSON.stringify(key)}:${encode(entry, roots)[0]}`).join(",")}}`,
|
|
102
|
-
values,
|
|
103
|
-
roots
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* The values a response sends for `addressed`, parents first. A value the browser holds is left
|
|
108
|
-
* out with everything inside it: the browser kept those when it read the value.
|
|
109
|
-
*/
|
|
110
|
-
function selectValuesToSend(addressed, held) {
|
|
111
|
-
const send = [];
|
|
112
|
-
const seen = /* @__PURE__ */ new Set();
|
|
113
|
-
const visit = (key) => {
|
|
114
|
-
if (seen.has(key)) return;
|
|
115
|
-
seen.add(key);
|
|
116
|
-
if (held?.has(key)) return;
|
|
117
|
-
const value = addressed.values.get(key);
|
|
118
|
-
send.push(value);
|
|
119
|
-
value.children.forEach(visit);
|
|
120
|
-
};
|
|
121
|
-
addressed.roots.forEach(visit);
|
|
122
|
-
return send;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* The value keys a request names (`F-Values`): well-formed keys only, at most `MAX_HELD_VALUES`.
|
|
126
|
-
*
|
|
127
|
-
* A response that leaves values out is one browser's. Integrations must keep it out of shared
|
|
128
|
-
* caches (CDN, ISR, PPR shells), and add `Vary: F-Values` to cacheable responses.
|
|
129
|
-
*/
|
|
130
|
-
function parseHeldValues(input) {
|
|
131
|
-
const header = typeof input === "string" || input == null ? input : (input instanceof Request ? input.headers : input).get(VALUES_HEADER);
|
|
132
|
-
const held = /* @__PURE__ */ new Set();
|
|
133
|
-
if (!header) return held;
|
|
134
|
-
for (const key of header.split(",", 256)) {
|
|
135
|
-
const trimmed = key.trim();
|
|
136
|
-
if (VALUE_KEY_PATTERN.test(trimmed)) held.add(trimmed);
|
|
137
|
-
if (held.size === 128) break;
|
|
138
|
-
}
|
|
139
|
-
return held;
|
|
140
|
-
}
|
|
141
|
-
/** A value script's JSON, safe inside a `<script>` element. */
|
|
142
|
-
function escapeScriptJson(json) {
|
|
143
|
-
return json.replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
144
|
-
}
|
|
3
|
+
import { a as encode, r as decode, t as EncodeError } from "./codec-C8to-RQW.mjs";
|
|
145
4
|
// src/server/seal.ts
|
|
146
5
|
/**
|
|
147
6
|
* Sealing an action's captured values. An inline `"use server"` function that reads a variable of
|
|
@@ -612,4 +471,4 @@ async function resolveAction(id, token) {
|
|
|
612
471
|
run: async (args) => runEntry(entry, values, args)
|
|
613
472
|
};
|
|
614
473
|
}
|
|
615
|
-
export {
|
|
474
|
+
export { getActionReference as a, resolveAction as c, generateActionKey as d, sealBound as f, encodeActionReference as i, SealError as l, configureActionKeys as n, registerInlineAction as o, unsealBound as p, decodeActionReference as r, registerServerReference as s, bindInlineAction as t, createActionKeys as u };
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
|
|
2
|
+
import { M as VALUE_SCRIPT_TYPE, h as ISLAND_ELEMENT, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "./wire-QEQWBr5s.mjs";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as ReactDOM from "react-dom";
|
|
5
|
+
// src/react/island.ts
|
|
6
|
+
/**
|
|
7
|
+
* A client component, as the server renders it: its HTML inside `<f-island>`, and its props in
|
|
8
|
+
* a script after it. Shared by the server and the browser: inside a hydrating island, a nested
|
|
9
|
+
* client component is part of that island's React tree, not an island of its own.
|
|
10
|
+
*
|
|
11
|
+
* The compiler wraps every export of a `"use client"` module with `createIsland` in the server
|
|
12
|
+
* build. The browser build keeps the module as it is.
|
|
13
|
+
*/
|
|
14
|
+
const CLIENT_REFERENCE = Symbol.for("islx.react.client-reference");
|
|
15
|
+
/** The module export an island component renders, or undefined for any other type. */
|
|
16
|
+
function getClientReference(type) {
|
|
17
|
+
if (!type || typeof type !== "function" && typeof type !== "object") return void 0;
|
|
18
|
+
return type[CLIENT_REFERENCE];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Makes `element`'s client components part of the React root around it (a layout that hydrates
|
|
22
|
+
* as one root), instead of islands of their own.
|
|
23
|
+
*/
|
|
24
|
+
function withinClientRoot(element) {
|
|
25
|
+
return React.createElement(getContexts().graph.Provider, { value: true }, element);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Starts islands again below a `withinClientRoot`: the page a hydrating layout keeps as server
|
|
29
|
+
* HTML renders its client components as islands of their own.
|
|
30
|
+
*/
|
|
31
|
+
function isolateIslands(element) {
|
|
32
|
+
return React.createElement(getContexts().graph.Provider, { value: false }, element);
|
|
33
|
+
}
|
|
34
|
+
const RENDER_STATE_RESOLVER = Symbol.for("islx.react.render-state");
|
|
35
|
+
/**
|
|
36
|
+
* For a framework that knows the request being rendered without React context (a request store):
|
|
37
|
+
* returns the render's settings when no `IslandsProvider` or `renderToStream` gave them. Keep the
|
|
38
|
+
* same `sent` set for the whole response.
|
|
39
|
+
*/
|
|
40
|
+
function setRenderStateResolver(resolve) {
|
|
41
|
+
globalThis[RENDER_STATE_RESOLVER] = resolve;
|
|
42
|
+
}
|
|
43
|
+
function resolveRenderState() {
|
|
44
|
+
const resolve = globalThis[RENDER_STATE_RESOLVER];
|
|
45
|
+
return resolve ? resolve() : null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Gives islands a render's settings when the framework renders the page itself (not through
|
|
49
|
+
* `renderToStream`): which values the browser holds, and which this render already wrote.
|
|
50
|
+
*/
|
|
51
|
+
function IslandsProvider({ held = null, children }) {
|
|
52
|
+
const state = React.useMemo(() => ({
|
|
53
|
+
held: held && held.size > 0 ? held : null,
|
|
54
|
+
sent: /* @__PURE__ */ new Set()
|
|
55
|
+
}), [held]);
|
|
56
|
+
return React.createElement(getContexts().render.Provider, { value: state }, children);
|
|
57
|
+
}
|
|
58
|
+
const SERVER_HOOK = Symbol.for("islx.react.server");
|
|
59
|
+
/** @internal */
|
|
60
|
+
function getIslandServerHook() {
|
|
61
|
+
return globalThis[SERVER_HOOK];
|
|
62
|
+
}
|
|
63
|
+
/** @internal */
|
|
64
|
+
function setIslandServerHook(hook) {
|
|
65
|
+
globalThis[SERVER_HOOK] = hook;
|
|
66
|
+
}
|
|
67
|
+
/** Tagged objects the runtime made (slots), as opposed to app data with a "$" key. */
|
|
68
|
+
const runtimeTagged = /* @__PURE__ */ new WeakSet();
|
|
69
|
+
const CONTEXTS = Symbol.for("islx.react.contexts");
|
|
70
|
+
/** @internal */
|
|
71
|
+
function getContexts() {
|
|
72
|
+
const scope = globalThis;
|
|
73
|
+
const all = scope[CONTEXTS] ??= /* @__PURE__ */ new WeakMap();
|
|
74
|
+
let contexts = all.get(React.createElement);
|
|
75
|
+
if (!contexts) {
|
|
76
|
+
contexts = {
|
|
77
|
+
graph: React.createContext(false),
|
|
78
|
+
render: React.createContext(null)
|
|
79
|
+
};
|
|
80
|
+
all.set(React.createElement, contexts);
|
|
81
|
+
}
|
|
82
|
+
return contexts;
|
|
83
|
+
}
|
|
84
|
+
function isElement(value) {
|
|
85
|
+
const type = value?.$$typeof;
|
|
86
|
+
return type === Symbol.for("react.transitional.element") || type === Symbol.for("react.element");
|
|
87
|
+
}
|
|
88
|
+
const slotStyle = { display: "contents" };
|
|
89
|
+
/**
|
|
90
|
+
* Splits props into what renders on the server and what travels: server output passed to an
|
|
91
|
+
* island (`children`, or any element prop) renders once inside `<f-slot>`, and travels as a
|
|
92
|
+
* reference to it, so the browser reads its HTML from the page instead of receiving it twice.
|
|
93
|
+
*/
|
|
94
|
+
function splitSlots(value, prefix, counter) {
|
|
95
|
+
if (isElement(value)) {
|
|
96
|
+
const name = `${prefix}${counter.next++}`;
|
|
97
|
+
const reference = {
|
|
98
|
+
["$"]: "s",
|
|
99
|
+
["v"]: name
|
|
100
|
+
};
|
|
101
|
+
runtimeTagged.add(reference);
|
|
102
|
+
return {
|
|
103
|
+
render: React.createElement(SLOT_ELEMENT, {
|
|
104
|
+
key: value.key ?? name,
|
|
105
|
+
name,
|
|
106
|
+
style: slotStyle
|
|
107
|
+
}, value),
|
|
108
|
+
wire: reference
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
const parts = value.map((entry) => splitSlots(entry, prefix, counter));
|
|
113
|
+
return {
|
|
114
|
+
render: parts.map((part) => part.render),
|
|
115
|
+
wire: parts.map((part) => part.wire)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (value && typeof value === "object") {
|
|
119
|
+
const prototype = Object.getPrototypeOf(value);
|
|
120
|
+
if (prototype !== Object.prototype && prototype !== null) return {
|
|
121
|
+
render: value,
|
|
122
|
+
wire: value
|
|
123
|
+
};
|
|
124
|
+
const render = {};
|
|
125
|
+
const wire = {};
|
|
126
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
127
|
+
const part = splitSlots(entry, prefix, counter);
|
|
128
|
+
render[key] = part.render;
|
|
129
|
+
wire[key] = part.wire;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
render,
|
|
133
|
+
wire
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
render: value,
|
|
138
|
+
wire: value
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const warned = /* @__PURE__ */ new Set();
|
|
142
|
+
const outcomes = /* @__PURE__ */ new WeakMap();
|
|
143
|
+
function settle(promise) {
|
|
144
|
+
let outcome = outcomes.get(promise);
|
|
145
|
+
if (!outcome) {
|
|
146
|
+
outcome = Promise.resolve(promise).then((value) => ({
|
|
147
|
+
ok: true,
|
|
148
|
+
value
|
|
149
|
+
}), (reason) => ({
|
|
150
|
+
ok: false,
|
|
151
|
+
reason
|
|
152
|
+
}));
|
|
153
|
+
outcomes.set(promise, outcome);
|
|
154
|
+
}
|
|
155
|
+
return outcome;
|
|
156
|
+
}
|
|
157
|
+
const REJECTED = "{\"e\":1}";
|
|
158
|
+
/** Each promise an island received, in a boundary of its own: its outcome once it settles. */
|
|
159
|
+
function promiseBoundaries(promises, label) {
|
|
160
|
+
return promises.map(({ name, promise }) => React.createElement(React.Suspense, {
|
|
161
|
+
key: name,
|
|
162
|
+
fallback: null
|
|
163
|
+
}, React.createElement(IslandPromise, {
|
|
164
|
+
name,
|
|
165
|
+
promise,
|
|
166
|
+
label
|
|
167
|
+
})));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* A promise's outcome, written when it settles. Its values are sent whether or not the page wrote
|
|
171
|
+
* them already: this script may land before an earlier island's, wherever React puts it.
|
|
172
|
+
*/
|
|
173
|
+
function IslandPromise({ name, promise, label }) {
|
|
174
|
+
const outcome = React.use(settle(promise));
|
|
175
|
+
const state = React.useContext(getContexts().render) ?? resolveRenderState();
|
|
176
|
+
const hook = getIslandServerHook();
|
|
177
|
+
let encoded = {
|
|
178
|
+
json: REJECTED,
|
|
179
|
+
values: [],
|
|
180
|
+
promises: []
|
|
181
|
+
};
|
|
182
|
+
if (outcome.ok) try {
|
|
183
|
+
encoded = hook.encode({ v: outcome.value }, state?.held ?? null);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.warn(`[islx] A promise passed to ${label} resolved to a value that cannot be sent to the browser: it rejects there.`, error);
|
|
186
|
+
}
|
|
187
|
+
else console.error(`[islx] A promise passed to ${label} rejected.`, outcome.reason);
|
|
188
|
+
return React.createElement(React.Fragment, null, ...encoded.values.map((value) => React.createElement("script", {
|
|
189
|
+
key: value.key,
|
|
190
|
+
type: VALUE_SCRIPT_TYPE,
|
|
191
|
+
name: value.key,
|
|
192
|
+
dangerouslySetInnerHTML: { __html: hook.escape(value.json) }
|
|
193
|
+
})), React.createElement("script", {
|
|
194
|
+
key: "outcome",
|
|
195
|
+
type: PROMISE_SCRIPT_TYPE,
|
|
196
|
+
name,
|
|
197
|
+
dangerouslySetInnerHTML: { __html: hook.escape(encoded.json) }
|
|
198
|
+
}), ...promiseBoundaries(encoded.promises, label));
|
|
199
|
+
}
|
|
200
|
+
function isThenable(value) {
|
|
201
|
+
return !!value && typeof value.then === "function";
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Wraps a client component so the server renders it as an island. `src` is the module the browser
|
|
205
|
+
* imports, `exportName` the component's export there.
|
|
206
|
+
*/
|
|
207
|
+
function createIsland(Component, src, exportName, strategy = "load") {
|
|
208
|
+
function Island(props) {
|
|
209
|
+
const { graph, render } = getContexts();
|
|
210
|
+
const insideIsland = React.useContext(graph);
|
|
211
|
+
const state = React.useContext(render) ?? resolveRenderState();
|
|
212
|
+
const id = React.useId();
|
|
213
|
+
if (insideIsland) return React.createElement(Component, props);
|
|
214
|
+
const hook = getIslandServerHook();
|
|
215
|
+
if (!hook) throw new Error("islx: an island rendered on a server that did not import @farm.js/islx/react/server");
|
|
216
|
+
let encoded;
|
|
217
|
+
let renderProps;
|
|
218
|
+
try {
|
|
219
|
+
const { render: rendered, wire } = splitSlots(props, `${id}s`, { next: 0 });
|
|
220
|
+
renderProps = rendered;
|
|
221
|
+
const skip = state ? /* @__PURE__ */ new Set([...state.held ?? [], ...state.sent]) : null;
|
|
222
|
+
encoded = hook.encode(wire, skip);
|
|
223
|
+
for (const value of encoded.values) state?.sent.add(value.key);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (isThenable(error)) throw error;
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
+
const key = `${src}#${exportName}:${message}`;
|
|
228
|
+
if (!warned.has(key)) {
|
|
229
|
+
warned.add(key);
|
|
230
|
+
console.warn(`[islx] ${exportName} (${src}) was rendered without hydration: its props cannot be sent to the browser. ${message}`);
|
|
231
|
+
}
|
|
232
|
+
return React.createElement(Component, props);
|
|
233
|
+
}
|
|
234
|
+
if (strategy === "load") for (const href of getBuildDefaults().modules?.[src] ?? []) ReactDOM.preloadModule?.(href, { as: "script" });
|
|
235
|
+
return React.createElement(React.Fragment, null, ...encoded.values.map((value) => React.createElement("script", {
|
|
236
|
+
key: value.key,
|
|
237
|
+
type: VALUE_SCRIPT_TYPE,
|
|
238
|
+
name: value.key,
|
|
239
|
+
dangerouslySetInnerHTML: { __html: hook.escape(value.json) }
|
|
240
|
+
})), React.createElement(ISLAND_ELEMENT, {
|
|
241
|
+
key: "island",
|
|
242
|
+
src,
|
|
243
|
+
export: exportName,
|
|
244
|
+
props: id,
|
|
245
|
+
strategy,
|
|
246
|
+
style: slotStyle
|
|
247
|
+
}, React.createElement(graph.Provider, { value: true }, React.createElement(Component, renderProps))), React.createElement("script", {
|
|
248
|
+
key: "props",
|
|
249
|
+
type: PROPS_SCRIPT_TYPE,
|
|
250
|
+
htmlFor: id,
|
|
251
|
+
dangerouslySetInnerHTML: { __html: hook.escape(encoded.json) }
|
|
252
|
+
}), ...promiseBoundaries(encoded.promises, `${exportName} (${src})`));
|
|
253
|
+
}
|
|
254
|
+
Island.displayName = `Island(${Component.displayName || Component.name || exportName})`;
|
|
255
|
+
Object.defineProperty(Island, CLIENT_REFERENCE, { value: {
|
|
256
|
+
src,
|
|
257
|
+
exportName,
|
|
258
|
+
Component
|
|
259
|
+
} });
|
|
260
|
+
return Island;
|
|
261
|
+
}
|
|
262
|
+
export { getIslandServerHook as a, setIslandServerHook as c, getContexts as i, setRenderStateResolver as l, createIsland as n, isolateIslands as o, getClientReference as r, runtimeTagged as s, IslandsProvider as t, withinClientRoot as u };
|