@farm.js/islx 0.0.3 → 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 +29 -0
- package/README.md +36 -1
- package/SPEC.md +11 -1
- package/dist/_chunks/{actions-2sTj0A2T.mjs → actions-8RQfrM-p.mjs} +2 -2
- package/dist/_chunks/{actions-B8dJ8f7L.mjs → actions-BuS5jDi0.mjs} +3 -144
- package/dist/_chunks/{codec-DqfVYKsx.mjs → codec-C8to-RQW.mjs} +3 -2
- package/dist/_chunks/island-DVOr-OUw.mjs +262 -0
- package/dist/_chunks/{schedule-BFoGKRrU.mjs → schedule-CjoybS3b.mjs} +1 -1
- package/dist/_chunks/{tree-BvYR6-PF.mjs → tree-BUkGP4AR.mjs} +5 -205
- package/dist/_chunks/values-37UIzxM0.mjs +144 -0
- package/dist/_chunks/vite-DG7hBn8A.mjs +503 -0
- package/dist/_chunks/{wire-C3Rpbwl6.d.mts → wire-Dipqh-EB.d.mts} +11 -1
- package/dist/_chunks/{wire-CXOodE06.mjs → wire-QEQWBr5s.mjs} +11 -1
- package/dist/client.mjs +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/react/client.d.mts +8 -2
- package/dist/react/client.mjs +148 -14
- package/dist/react/server.mjs +25 -5
- package/dist/react.mjs +3 -1
- package/dist/server.mjs +4 -3
- 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/dist/react/client.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-
|
|
2
|
-
import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { n as
|
|
1
|
+
import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-8RQfrM-p.mjs";
|
|
2
|
+
import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-C8to-RQW.mjs";
|
|
3
|
+
import { F as getTag, f as FORM_STATE_SCRIPT_TYPE, h as ISLAND_ELEMENT, m as HYDRATED_ATTRIBUTE, w as SLOT_ELEMENT, x as PROPS_SCRIPT_TYPE, y as PROMISE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
|
|
4
|
+
import { i as getContexts, o as isolateIslands, u as withinClientRoot } from "../_chunks/island-DVOr-OUw.mjs";
|
|
5
|
+
import { i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement } from "../_chunks/tree-BUkGP4AR.mjs";
|
|
6
|
+
import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-CjoybS3b.mjs";
|
|
6
7
|
import * as React from "react";
|
|
7
8
|
import { hydrateRoot } from "react-dom/client";
|
|
8
9
|
// src/react/hydrate.ts
|
|
@@ -39,22 +40,22 @@ function readFormState(document) {
|
|
|
39
40
|
const revivedValues = /* @__PURE__ */ new WeakMap();
|
|
40
41
|
let slotsRevived = 0;
|
|
41
42
|
/** Turns travelled props back into values: slots keep their server HTML, actions become callable. */
|
|
42
|
-
function revive(value, container) {
|
|
43
|
+
function revive(value, container, values) {
|
|
43
44
|
if (value && typeof value === "object") {
|
|
44
45
|
const cached = revivedValues.get(value);
|
|
45
46
|
if (cached !== void 0) return cached;
|
|
46
47
|
const slotsBefore = slotsRevived;
|
|
47
|
-
const revived = reviveUncached(value, container);
|
|
48
|
+
const revived = reviveUncached(value, container, values);
|
|
48
49
|
if (slotsRevived === slotsBefore) revivedValues.set(value, revived);
|
|
49
50
|
return revived;
|
|
50
51
|
}
|
|
51
52
|
return value;
|
|
52
53
|
}
|
|
53
|
-
function reviveUncached(value, container) {
|
|
54
|
+
function reviveUncached(value, container, values) {
|
|
54
55
|
if (Array.isArray(value)) {
|
|
55
56
|
let changed = false;
|
|
56
57
|
const revived = value.map((entry) => {
|
|
57
|
-
const next = revive(entry, container);
|
|
58
|
+
const next = revive(entry, container, values);
|
|
58
59
|
if (next !== entry) changed = true;
|
|
59
60
|
return next;
|
|
60
61
|
});
|
|
@@ -82,25 +83,152 @@ function reviveUncached(value, container) {
|
|
|
82
83
|
dangerouslySetInnerHTML: { __html: html }
|
|
83
84
|
});
|
|
84
85
|
}
|
|
86
|
+
if (tag === "T" && typeof object["v"] === "string") return islandPromise(object["v"], container, values);
|
|
85
87
|
if (tag === "r") throw new Error(`islx value ${String(object["v"])} was not resolved`);
|
|
86
88
|
if (tag !== void 0) {
|
|
87
|
-
const decoded = decodeTagged(object, (child) => revive(child, container));
|
|
89
|
+
const decoded = decodeTagged(object, (child) => revive(child, container, values));
|
|
88
90
|
if (decoded !== NOT_A_VALUE) return decoded;
|
|
89
91
|
}
|
|
90
92
|
let changed = false;
|
|
91
93
|
const revived = {};
|
|
92
94
|
for (const [key, entry] of Object.entries(object)) {
|
|
93
|
-
const next = revive(entry, container);
|
|
95
|
+
const next = revive(entry, container, values);
|
|
94
96
|
setOwn(revived, key, next);
|
|
95
97
|
if (next !== entry) changed = true;
|
|
96
98
|
}
|
|
97
99
|
return changed ? revived : value;
|
|
98
100
|
}
|
|
101
|
+
const pendingPromises = /* @__PURE__ */ new Map();
|
|
102
|
+
let promiseObserver = null;
|
|
103
|
+
function findPromiseScript(container, name) {
|
|
104
|
+
const documents = new Set([container.ownerDocument, globalThis.document].filter(Boolean));
|
|
105
|
+
for (const document of documents) for (const script of document.querySelectorAll(`script[type="${PROMISE_SCRIPT_TYPE}"]`)) if (script.getAttribute("name") === name) return script;
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
function readOutcome(script, container, values) {
|
|
109
|
+
try {
|
|
110
|
+
const text = script.textContent || "{}";
|
|
111
|
+
let parsed = JSON.parse(text);
|
|
112
|
+
if (!parsed || typeof parsed !== "object" || !("v" in parsed)) return {
|
|
113
|
+
ok: false,
|
|
114
|
+
reason: /* @__PURE__ */ new Error("islx: the promise rejected on the server")
|
|
115
|
+
};
|
|
116
|
+
if (text.includes(`"\$":"r"`)) {
|
|
117
|
+
values.ingest(container.ownerDocument);
|
|
118
|
+
parsed = values.resolve(parsed);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
value: revive(parsed, container, values).v
|
|
123
|
+
};
|
|
124
|
+
} catch (reason) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
reason
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The promise an island received, as a thenable React's `use` reads: settled already when its
|
|
133
|
+
* outcome is on the page, so hydration does not suspend, and settled when the outcome streams in
|
|
134
|
+
* otherwise.
|
|
135
|
+
*/
|
|
136
|
+
function islandPromise(name, container, values) {
|
|
137
|
+
const pending = pendingPromises.get(name);
|
|
138
|
+
if (pending) return pending.thenable;
|
|
139
|
+
const script = findPromiseScript(container, name);
|
|
140
|
+
if (script) {
|
|
141
|
+
const outcome = readOutcome(script, container, values);
|
|
142
|
+
const thenable = outcome.ok ? Promise.resolve(outcome.value) : Promise.reject(outcome.reason);
|
|
143
|
+
if (outcome.ok) Object.assign(thenable, {
|
|
144
|
+
status: "fulfilled",
|
|
145
|
+
value: outcome.value
|
|
146
|
+
});
|
|
147
|
+
else {
|
|
148
|
+
thenable.catch(() => {});
|
|
149
|
+
Object.assign(thenable, {
|
|
150
|
+
status: "rejected",
|
|
151
|
+
reason: outcome.reason
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return thenable;
|
|
155
|
+
}
|
|
156
|
+
let resolve;
|
|
157
|
+
let reject;
|
|
158
|
+
const thenable = new Promise((done, fail) => {
|
|
159
|
+
resolve = done;
|
|
160
|
+
reject = fail;
|
|
161
|
+
});
|
|
162
|
+
thenable.catch(() => {});
|
|
163
|
+
thenable.status = "pending";
|
|
164
|
+
pendingPromises.set(name, {
|
|
165
|
+
thenable,
|
|
166
|
+
container,
|
|
167
|
+
values,
|
|
168
|
+
settle(found) {
|
|
169
|
+
pendingPromises.delete(name);
|
|
170
|
+
const outcome = readOutcome(found, container, values);
|
|
171
|
+
if (outcome.ok) {
|
|
172
|
+
Object.assign(thenable, {
|
|
173
|
+
status: "fulfilled",
|
|
174
|
+
value: outcome.value
|
|
175
|
+
});
|
|
176
|
+
resolve(outcome.value);
|
|
177
|
+
} else {
|
|
178
|
+
Object.assign(thenable, {
|
|
179
|
+
status: "rejected",
|
|
180
|
+
reason: outcome.reason
|
|
181
|
+
});
|
|
182
|
+
reject(outcome.reason);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
if (!promiseObserver && typeof MutationObserver === "function") {
|
|
187
|
+
promiseObserver = new MutationObserver(() => {
|
|
188
|
+
for (const [pendingName, entry] of pendingPromises) {
|
|
189
|
+
const found = findPromiseScript(entry.container, pendingName);
|
|
190
|
+
if (found) entry.settle(found);
|
|
191
|
+
}
|
|
192
|
+
if (pendingPromises.size === 0) {
|
|
193
|
+
promiseObserver?.disconnect();
|
|
194
|
+
promiseObserver = null;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
promiseObserver.observe(globalThis.document ?? container.ownerDocument, {
|
|
198
|
+
childList: true,
|
|
199
|
+
subtree: true
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return thenable;
|
|
203
|
+
}
|
|
99
204
|
/**
|
|
100
205
|
* React streams Suspense content that resolves late into a hidden `<div id="S:n">`, then moves it
|
|
101
206
|
* into place. Its islands hydrate once moved, not while staged.
|
|
102
207
|
*/
|
|
103
208
|
const STREAM_STAGING = "div[hidden][id^=\"S:\"]";
|
|
209
|
+
const PENDING_BOUNDARY = "$?";
|
|
210
|
+
/**
|
|
211
|
+
* Whether React still streams Suspense content into `container`. React waits for such a boundary
|
|
212
|
+
* only while the document loads; later (a navigation that streams) it would render it again in
|
|
213
|
+
* the browser, so the island hydrates once React has revealed it.
|
|
214
|
+
*/
|
|
215
|
+
function awaitsStreamedContent(container) {
|
|
216
|
+
const document = container.ownerDocument;
|
|
217
|
+
if (document.readyState === "loading") return false;
|
|
218
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT);
|
|
219
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) if (node.data === PENDING_BOUNDARY) return true;
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Marks the Suspense boundaries under `root` that still wait once their stream has ended (the
|
|
224
|
+
* connection dropped) as rendered in the browser, as React does when it finds one: the next
|
|
225
|
+
* `hydrate()` pass starts their islands. Call it when a stream that filled `root` ends.
|
|
226
|
+
*/
|
|
227
|
+
function endStreamedContent(root) {
|
|
228
|
+
const node = root;
|
|
229
|
+
const walker = (node.nodeType === 9 ? node : node.ownerDocument).createTreeWalker(node, NodeFilter.SHOW_COMMENT);
|
|
230
|
+
for (let comment = walker.nextNode(); comment; comment = walker.nextNode()) if (comment.data === PENDING_BOUNDARY) comment.data = "$!";
|
|
231
|
+
}
|
|
104
232
|
/** The hydration runtime for one page. */
|
|
105
233
|
function createHydrationRuntime(options) {
|
|
106
234
|
const roots = /* @__PURE__ */ new Map();
|
|
@@ -169,7 +297,7 @@ function createHydrationRuntime(options) {
|
|
|
169
297
|
values.ingest(container.ownerDocument);
|
|
170
298
|
parsed = values.resolve(parsed);
|
|
171
299
|
}
|
|
172
|
-
return revive(parsed, container);
|
|
300
|
+
return revive(parsed, container, values);
|
|
173
301
|
}
|
|
174
302
|
const topLevel = (scope) => Array.from(scope.querySelectorAll(ISLAND_SELECTOR)).filter((container) => !container.parentElement?.closest(ISLAND_SELECTOR));
|
|
175
303
|
const keyOf = (container) => `${container.getAttribute("src")}#${container.getAttribute("export") || "default"}`;
|
|
@@ -182,7 +310,7 @@ function createHydrationRuntime(options) {
|
|
|
182
310
|
if (scope instanceof Document) watchStream(scope);
|
|
183
311
|
const candidates = Array.from(scope.querySelectorAll(ISLAND_SELECTOR));
|
|
184
312
|
if (scope instanceof Element && scope.matches(ISLAND_SELECTOR)) candidates.unshift(scope);
|
|
185
|
-
const islands = candidates.filter((container) => !roots.has(container) && !pending.has(container) && !container.parentElement?.closest(ISLAND_SELECTOR) && !container.closest(STREAM_STAGING));
|
|
313
|
+
const islands = candidates.filter((container) => !roots.has(container) && !pending.has(container) && !container.parentElement?.closest(ISLAND_SELECTOR) && !container.closest(STREAM_STAGING) && !awaitsStreamedContent(container));
|
|
186
314
|
await Promise.all(islands.map(async (container) => {
|
|
187
315
|
const src = container.getAttribute("src");
|
|
188
316
|
const id = container.getAttribute("props");
|
|
@@ -350,6 +478,7 @@ function createHydrationRuntime(options) {
|
|
|
350
478
|
if (document.readyState !== "loading" && !document.querySelector(STREAM_STAGING)) {
|
|
351
479
|
observer.disconnect();
|
|
352
480
|
watching = false;
|
|
481
|
+
endStreamedContent(document);
|
|
353
482
|
}
|
|
354
483
|
hydrate(document).catch((error) => report("Hydrating streamed islands failed.", error));
|
|
355
484
|
};
|
|
@@ -366,6 +495,11 @@ function createHydrationRuntime(options) {
|
|
|
366
495
|
document.addEventListener("DOMContentLoaded", run, { once: true });
|
|
367
496
|
}
|
|
368
497
|
function dispose(scope) {
|
|
498
|
+
for (const [name, entry] of pendingPromises) if (scope.contains(entry.container)) pendingPromises.delete(name);
|
|
499
|
+
if (pendingPromises.size === 0) {
|
|
500
|
+
promiseObserver?.disconnect();
|
|
501
|
+
promiseObserver = null;
|
|
502
|
+
}
|
|
369
503
|
for (const [container, controller] of pending) if (container === scope || scope.contains(container)) {
|
|
370
504
|
controller.abort();
|
|
371
505
|
pending.delete(container);
|
|
@@ -453,4 +587,4 @@ async function swapDocument(runtime, next) {
|
|
|
453
587
|
document.body.replaceChildren(...Array.from(body.childNodes));
|
|
454
588
|
await runtime.hydrate(document);
|
|
455
589
|
}
|
|
456
|
-
export { HYDRATED_ATTRIBUTE, ISLAND_STRATEGIES, PAYLOAD_ONLY_ATTRIBUTE, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
|
|
590
|
+
export { HYDRATED_ATTRIBUTE, ISLAND_STRATEGIES, PAYLOAD_ONLY_ATTRIBUTE, createHydrationRuntime, createServerHtml, createServerTreeElement, createServerTreeLayout, endStreamedContent, isIslandStrategy, isolateIslands, refreshDocument, scheduleHydration, swapDocument, updateServerTrees, withinClientRoot };
|
package/dist/react/server.mjs
CHANGED
|
@@ -1,19 +1,38 @@
|
|
|
1
|
-
import { a as encode } from "../_chunks/codec-
|
|
2
|
-
import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-
|
|
1
|
+
import { a as encode } from "../_chunks/codec-C8to-RQW.mjs";
|
|
2
|
+
import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-QEQWBr5s.mjs";
|
|
3
3
|
import { r as PREHYDRATION_SCRIPT } from "../_chunks/prehydration-B1GLTLdk.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { i as encodeActionReference } from "../_chunks/actions-BuS5jDi0.mjs";
|
|
5
5
|
import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
|
|
6
|
-
import { a as
|
|
6
|
+
import { a as parseHeldValues, n as addressValues, o as selectValuesToSend, r as escapeScriptJson } from "../_chunks/values-37UIzxM0.mjs";
|
|
7
|
+
import { c as setIslandServerHook, i as getContexts, l as setRenderStateResolver, n as createIsland, o as isolateIslands, r as getClientReference, s as runtimeTagged, t as IslandsProvider, u as withinClientRoot } from "../_chunks/island-DVOr-OUw.mjs";
|
|
8
|
+
import { a as renderServerTree, n as PAYLOAD_ONLY_ATTRIBUTE, t as Outlet } from "../_chunks/tree-BUkGP4AR.mjs";
|
|
7
9
|
import * as React from "react";
|
|
8
10
|
// src/react/server.ts
|
|
9
11
|
/**
|
|
10
12
|
* @farm.js/islx/react/server: rendering a React page with islands. Importing it gives islands the
|
|
11
13
|
* server code they need (values, action references); `renderToStream` renders a page.
|
|
12
14
|
*/
|
|
15
|
+
const PROMISE_PREFIX = `${Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(2)), (part) => part.toString(36)).join("")}.`;
|
|
16
|
+
let promiseCount = 0;
|
|
17
|
+
function isThenable(value) {
|
|
18
|
+
return value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
|
|
19
|
+
}
|
|
13
20
|
setIslandServerHook({
|
|
14
21
|
encode(props, held) {
|
|
22
|
+
const promises = [];
|
|
15
23
|
const addressed = addressValues(props, { encodeSpecial: (value) => {
|
|
16
24
|
if (value && typeof value === "object" && runtimeTagged.has(value)) return value;
|
|
25
|
+
if (isThenable(value)) {
|
|
26
|
+
const name = PROMISE_PREFIX + (promiseCount++).toString(36);
|
|
27
|
+
promises.push({
|
|
28
|
+
name,
|
|
29
|
+
promise: value
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
["$"]: "T",
|
|
33
|
+
["v"]: name
|
|
34
|
+
};
|
|
35
|
+
}
|
|
17
36
|
if (typeof value === "function") {
|
|
18
37
|
const reference = encodeActionReference(value);
|
|
19
38
|
if (reference) return reference;
|
|
@@ -26,7 +45,8 @@ setIslandServerHook({
|
|
|
26
45
|
values: selectValuesToSend(addressed, held).map(({ key, json }) => ({
|
|
27
46
|
key,
|
|
28
47
|
json
|
|
29
|
-
}))
|
|
48
|
+
})),
|
|
49
|
+
promises
|
|
30
50
|
};
|
|
31
51
|
},
|
|
32
52
|
escape: escapeScriptJson,
|
package/dist/react.mjs
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
import { n as createIsland, o as isolateIslands, t as IslandsProvider, u as withinClientRoot } from "./_chunks/island-DVOr-OUw.mjs";
|
|
2
|
+
import { a as renderServerTree, t as Outlet } from "./_chunks/tree-BUkGP4AR.mjs";
|
|
3
|
+
export { IslandsProvider, Outlet, createIsland, isolateIslands, renderServerTree, withinClientRoot };
|
package/dist/server.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
1
|
+
import { a as parseHeldValues, i as hashValue, n as addressValues, o as selectValuesToSend, r as escapeScriptJson, t as VALUE_MIN_LENGTH } from "./_chunks/values-37UIzxM0.mjs";
|
|
2
|
+
import { a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, i as encodeActionReference, l as SealError, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys } from "./_chunks/actions-BuS5jDi0.mjs";
|
|
3
|
+
import { a as encode, r as decode } from "./_chunks/codec-C8to-RQW.mjs";
|
|
4
|
+
import { I as readPostedAction, L as stripRuntimeFields, N as attachmentField, n as ACTION_KEY_FIELD, o as BOUND_FIELD, r as ACTION_STALE_HEADER, s as CALL_FIELD, t as ACTION_HEADER } from "./_chunks/wire-QEQWBr5s.mjs";
|
|
4
5
|
import { n as setBuildDefaults, t as getBuildDefaults } from "./_chunks/build-COIcs7wm.mjs";
|
|
5
6
|
// src/server/hooks.ts
|
|
6
7
|
const HOOKS_KEY = Symbol.for("islx.server.hooks");
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { HandlerCallback } from "@tanstack/react-router/ssr/server";
|
|
2
|
+
import { AnyRouter } from "@tanstack/react-router";
|
|
3
|
+
|
|
4
|
+
// src/tanstack-start/server.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Renders the page with its islands: values the browser holds stay out, and a navigation (an
|
|
8
|
+
* `F-Render` request) gets the whole page in one piece, as islx's browser runtime swaps it in.
|
|
9
|
+
*/
|
|
10
|
+
declare const islandsHandler: HandlerCallback<AnyRouter>;
|
|
11
|
+
export { islandsHandler };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { t as IslandsProvider } from "../_chunks/island-DVOr-OUw.mjs";
|
|
2
|
+
import { A as VALUES_HEADER, C as RENDER_HEADER } from "../_chunks/wire-QEQWBr5s.mjs";
|
|
3
|
+
import { a as parseHeldValues } from "../_chunks/values-37UIzxM0.mjs";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { StartServer } from "@tanstack/react-start/server";
|
|
6
|
+
import { createSsrStreamResponse, defineHandlerCallback, getSsrStatus, renderRouterToStream, transformReadableStreamWithRouter } from "@tanstack/react-router/ssr/server";
|
|
7
|
+
// src/tanstack-start/server.ts
|
|
8
|
+
/**
|
|
9
|
+
* @farm.js/islx/tanstack-start/server: the render handler of a TanStack Start app as islands.
|
|
10
|
+
* `islxTanstackStart()` uses it for Start's default server entry; an app with its own
|
|
11
|
+
* `src/server.ts` passes it to `createStartHandler`:
|
|
12
|
+
*
|
|
13
|
+
* import { createServerEntry, createStartHandler } from "@tanstack/react-start/server";
|
|
14
|
+
* import { islandsHandler } from "@farm.js/islx/tanstack-start/server";
|
|
15
|
+
* export default createServerEntry({ fetch: createStartHandler(islandsHandler) });
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Renders the page with its islands: values the browser holds stay out, and a navigation (an
|
|
19
|
+
* `F-Render` request) gets the whole page in one piece, as islx's browser runtime swaps it in.
|
|
20
|
+
*/
|
|
21
|
+
const islandsHandler = defineHandlerCallback(({ request, router, responseHeaders }) => {
|
|
22
|
+
const held = parseHeldValues(request);
|
|
23
|
+
responseHeaders.append("Vary", `${VALUES_HEADER}, ${RENDER_HEADER}`);
|
|
24
|
+
if (held.size > 0) responseHeaders.set("Cache-Control", "private, no-store");
|
|
25
|
+
const children = React.createElement(IslandsProvider, { held }, React.createElement(StartServer, { router }));
|
|
26
|
+
if (!request.headers.has("F-Render")) return renderRouterToStream({
|
|
27
|
+
request,
|
|
28
|
+
router,
|
|
29
|
+
responseHeaders,
|
|
30
|
+
children
|
|
31
|
+
});
|
|
32
|
+
return renderWhole(request, router, responseHeaders, children);
|
|
33
|
+
});
|
|
34
|
+
/** Waits for every Suspense boundary, so React writes the page with no streaming scripts. */
|
|
35
|
+
async function renderWhole(request, router, responseHeaders, children) {
|
|
36
|
+
const { renderToReadableStream } = await import("react-dom/server");
|
|
37
|
+
const stream = await renderToReadableStream(children, {
|
|
38
|
+
signal: request.signal,
|
|
39
|
+
nonce: router.options.ssr?.nonce,
|
|
40
|
+
onError(error) {
|
|
41
|
+
if (!request.signal.aborted) console.error("Error in renderToReadableStream:", error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
await stream.allReady;
|
|
45
|
+
const body = transformReadableStreamWithRouter(router, stream, {
|
|
46
|
+
rendererSafePoint: "script-close",
|
|
47
|
+
signal: request.signal
|
|
48
|
+
});
|
|
49
|
+
return createSsrStreamResponse(router, new Response(body, {
|
|
50
|
+
status: getSsrStatus(router),
|
|
51
|
+
headers: responseHeaders
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
export { islandsHandler };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { IslxOptions } from "./vite.mjs";
|
|
2
|
+
import { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
// src/tanstack-start/index.d.ts
|
|
5
|
+
|
|
6
|
+
type IslxTanstackStartOptions = IslxOptions;
|
|
7
|
+
declare function islxTanstackStart(options?: IslxTanstackStartOptions): Plugin[];
|
|
8
|
+
export { IslxTanstackStartOptions, islxTanstackStart as default, islxTanstackStart };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { n as islx, t as ENTRY_ID } from "./_chunks/vite-DG7hBn8A.mjs";
|
|
2
|
+
import { normalizePath } from "vite";
|
|
3
|
+
// src/tanstack-start/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* @farm.js/islx/tanstack-start: a TanStack Start app as islands.
|
|
6
|
+
*
|
|
7
|
+
* import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
8
|
+
* import { islxTanstackStart } from "@farm.js/islx/tanstack-start";
|
|
9
|
+
* export default defineConfig({ plugins: [tanstackStart(), react(), islxTanstackStart()] });
|
|
10
|
+
*
|
|
11
|
+
* Routes render on the server; the browser loads islx and the `"use client"` components a page
|
|
12
|
+
* uses, not the router or the routes. TanStack Start's client entry becomes islx's browser entry,
|
|
13
|
+
* so `<Scripts />` still loads it. Client components are the `"use client"` modules under `src/`
|
|
14
|
+
* (`clientComponents.include`); a library's stay server HTML.
|
|
15
|
+
*/
|
|
16
|
+
const START_CLIENT_ENTRY = "virtual:tanstack-start-client-entry";
|
|
17
|
+
const START_SERVER_ENTRY = "virtual:tanstack-start-server-entry";
|
|
18
|
+
const START_ROUTER_ENTRY = "#tanstack-router-entry";
|
|
19
|
+
const ROUTER_WRAPPER = "\0islx:tanstack-router";
|
|
20
|
+
const SERVER_WRAPPER = "\0islx:tanstack-server-entry";
|
|
21
|
+
const SERVER_ENTRY = `import { createServerEntry, createStartHandler } from "@tanstack/react-start/server";
|
|
22
|
+
import { handleAction } from "@farm.js/islx/server";
|
|
23
|
+
import { islandsHandler } from "@farm.js/islx/tanstack-start/server";
|
|
24
|
+
const start = createStartHandler(islandsHandler);
|
|
25
|
+
export default createServerEntry({
|
|
26
|
+
// islx's server actions first; any other request (and Start's server functions) goes to Start.
|
|
27
|
+
async fetch(request, ...rest) {
|
|
28
|
+
return (await handleAction(request)) ?? start(request, ...rest);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
`;
|
|
32
|
+
const routerWrapper = (routerPath) => `import * as app from ${JSON.stringify(routerPath)};
|
|
33
|
+
export * from ${JSON.stringify(routerPath)};
|
|
34
|
+
export async function getRouter(...args) {
|
|
35
|
+
const router = await app.getRouter(...args);
|
|
36
|
+
const lifecycle = (router.serverSsrLifecycle ??= {});
|
|
37
|
+
// Start dehydrates every request: here that turns hydration off instead.
|
|
38
|
+
(lifecycle.onServerSsrAttach ??= []).push((ssr) => {
|
|
39
|
+
ssr.dehydrate = async () => ssr.disableHydration();
|
|
40
|
+
});
|
|
41
|
+
return router;
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
function islxTanstackStart(options = {}) {
|
|
45
|
+
let clientEntry;
|
|
46
|
+
let serverEntry;
|
|
47
|
+
let routerEntry;
|
|
48
|
+
const pathOf = (id) => normalizePath(id.split("?", 1)[0]);
|
|
49
|
+
return [{
|
|
50
|
+
name: "islx:tanstack-start",
|
|
51
|
+
enforce: "pre",
|
|
52
|
+
configResolved(config) {
|
|
53
|
+
const aliasOf = (find) => {
|
|
54
|
+
const alias = config.resolve.alias.find((entry) => entry.find === find);
|
|
55
|
+
if (!alias) throw new Error("islx: add tanstackStart() from @tanstack/react-start/plugin/vite before islxTanstackStart()");
|
|
56
|
+
return normalizePath(alias.replacement);
|
|
57
|
+
};
|
|
58
|
+
clientEntry = aliasOf(START_CLIENT_ENTRY);
|
|
59
|
+
routerEntry = aliasOf(START_ROUTER_ENTRY);
|
|
60
|
+
const server = aliasOf(START_SERVER_ENTRY);
|
|
61
|
+
if (server.includes("/node_modules/") || server.includes("/@tanstack/")) serverEntry = server;
|
|
62
|
+
},
|
|
63
|
+
resolveId(id, importer) {
|
|
64
|
+
if (this.environment.name === "client" || !routerEntry) return void 0;
|
|
65
|
+
if (serverEntry && pathOf(id) === serverEntry) return SERVER_WRAPPER;
|
|
66
|
+
if (!importer) return void 0;
|
|
67
|
+
if (pathOf(id) === routerEntry && importer !== ROUTER_WRAPPER && importer.includes("@tanstack")) return ROUTER_WRAPPER;
|
|
68
|
+
},
|
|
69
|
+
load(id) {
|
|
70
|
+
if (id === ROUTER_WRAPPER) return routerWrapper(routerEntry);
|
|
71
|
+
if (id === SERVER_WRAPPER) return SERVER_ENTRY;
|
|
72
|
+
const file = pathOf(id);
|
|
73
|
+
if (this.environment.name === "client") return file === clientEntry ? `import ${JSON.stringify(ENTRY_ID)};` : void 0;
|
|
74
|
+
}
|
|
75
|
+
}, ...islx({
|
|
76
|
+
serverEnvironments: ["ssr"],
|
|
77
|
+
...options,
|
|
78
|
+
clientComponents: {
|
|
79
|
+
include: options.clientComponents?.include ?? ["src/**"],
|
|
80
|
+
exclude: ["**/node_modules/**", ...options.clientComponents?.exclude ?? []]
|
|
81
|
+
}
|
|
82
|
+
}, {
|
|
83
|
+
frameworkEntry: true,
|
|
84
|
+
frameworkBuild: true,
|
|
85
|
+
frameworkRender: true
|
|
86
|
+
})];
|
|
87
|
+
}
|
|
88
|
+
export { islxTanstackStart as default, islxTanstackStart };
|
package/dist/vite.d.mts
CHANGED
|
@@ -43,6 +43,8 @@ interface IslxOptions {
|
|
|
43
43
|
server?: string | ServerHooks;
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
/** @internal The browser entry, for a framework whose own entry imports it. */
|
|
47
|
+
declare const ENTRY_ID = "virtual:islx/client-entry";
|
|
46
48
|
/**
|
|
47
49
|
* The source of a hooks module made from callbacks. Each callback is copied as written, so it
|
|
48
50
|
* must not use anything from where it was written: an outside variable, an import.
|
|
@@ -53,5 +55,22 @@ declare function resolveDirectives(directives?: IslxOptions["directives"]): {
|
|
|
53
55
|
client: string;
|
|
54
56
|
server: string;
|
|
55
57
|
};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
/**
|
|
59
|
+
* @internal How a framework integration hosts islx (`@farm.js/islx/tanstack-start`).
|
|
60
|
+
*/
|
|
61
|
+
interface IslxHost {
|
|
62
|
+
/** The framework's own client entry imports islx's, so islx adds no browser entry of its own. */
|
|
63
|
+
frameworkEntry?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The framework renders pages itself, without `renderToStream`: the server runtime islands need
|
|
66
|
+
* loads with them.
|
|
67
|
+
*/
|
|
68
|
+
frameworkRender?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* The framework orders the build (the browser before the server): client modules come from
|
|
71
|
+
* `clientComponents.include`, and the server build gets their files directly.
|
|
72
|
+
*/
|
|
73
|
+
frameworkBuild?: boolean;
|
|
74
|
+
}
|
|
75
|
+
declare function islx(options?: IslxOptions, host?: IslxHost): Plugin[];
|
|
76
|
+
export { ENTRY_ID, IslxHost, IslxOptions, islx as default, islx, resolveDirectives, serializeHooks };
|