@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.
@@ -1,8 +1,9 @@
1
- import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-2sTj0A2T.mjs";
2
- import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-DqfVYKsx.mjs";
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 { 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
- import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-BFoGKRrU.mjs";
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 };
@@ -1,19 +1,38 @@
1
- import { a as encode } from "../_chunks/codec-DqfVYKsx.mjs";
2
- import { f as FORM_STATE_SCRIPT_TYPE } from "../_chunks/wire-CXOodE06.mjs";
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 { g as escapeScriptJson, h as addressValues, i as encodeActionReference, v as parseHeldValues, y as selectValuesToSend } from "../_chunks/actions-B8dJ8f7L.mjs";
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 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";
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
- 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";
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 { _ as hashValue, a as getActionReference, c as resolveAction, d as generateActionKey, f as sealBound, g as escapeScriptJson, h as addressValues, i as encodeActionReference, l as SealError, m as VALUE_MIN_LENGTH, n as configureActionKeys, o as registerInlineAction, p as unsealBound, r as decodeActionReference, s as registerServerReference, t as bindInlineAction, u as createActionKeys, v as parseHeldValues, y as selectValuesToSend } from "./_chunks/actions-B8dJ8f7L.mjs";
2
- import { a as encode, r as decode } from "./_chunks/codec-DqfVYKsx.mjs";
3
- import { F as stripRuntimeFields, P as readPostedAction, j 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-CXOodE06.mjs";
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
- declare function islx(options?: IslxOptions): Plugin[];
57
- export { IslxOptions, islx as default, islx, resolveDirectives, serializeHooks };
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 };