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