@farm.js/islx 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.6
4
+
5
+ Server action interceptor, early clicks on TanStack Start, library islands under pnpm.
6
+
7
+ - `setActionInterceptor((call, next) => …)` from `@farm.js/islx/client` wraps every server
8
+ action call, references decoded from props included. It sees the call when it is made
9
+ (`{ id, name, args }`) and `next` sends it in queue order, so a framework can unwrap what its
10
+ server side adds to a result (cache invalidations, a query's cache entry, a declared failure).
11
+ - TanStack Start: a click on a button, or a form submit, in an island before it hydrates is held
12
+ and replayed, as in a plain islx app (the pre-hydration script goes in the root route's head).
13
+ - A library's `"use client"` components (islands the Vite plugin makes in a bundled server build)
14
+ import islx's island runtime from the app instead of from the library's folder. Under pnpm the
15
+ library's folder in the store cannot see `@farm.js/islx`, so the server build failed to resolve
16
+ `@farm.js/islx/react`; now it gets the same copy the app's pages use.
17
+
3
18
  ## 0.0.5
4
19
 
5
20
  TanStack Start.
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.5. React, Vite and TanStack Start are supported; other renderers and framework
16
+ > Status: 0.0.6. 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
 
@@ -186,7 +186,9 @@ export default defineConfig({ plugins: [tanstackStart(), react(), islxTanstackSt
186
186
  Routes, loaders and layouts render on the server. The browser loads islx and the `"use client"`
187
187
  components a page uses (the modules under `src/`), not the router, the route tree or loader
188
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
189
+ dehydrated. A click on a button (or a form submit) in an island before it hydrates is held and
190
+ replayed. Links move between
191
+ routes without loading a document. A loader's promise passed
190
192
  unawaited to a client component streams in. Islands call Start server functions
191
193
  (`createServerFn`) and islx server actions (`"use server"`).
192
194
 
@@ -197,8 +199,6 @@ unawaited to a client component streams in. Islands call Start server functions
197
199
  - Client components run without a router in the browser: they cannot use `useRouter`,
198
200
  `useNavigate` or `<Link>` (render plain `<a>` links; the server renders `<Link>` in routes).
199
201
  - 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
202
  - Start sends a bot the whole page before any of it; people get it streamed.
203
203
  - Start needs Node 22.12 or later.
204
204
 
@@ -151,6 +151,10 @@ function state() {
151
151
  function setActionNavigator(navigator) {
152
152
  state().navigator = navigator;
153
153
  }
154
+ /** Installs the framework's interceptor for server action calls (one; a later call replaces it). */
155
+ function setActionInterceptor(interceptor) {
156
+ state().interceptor = interceptor;
157
+ }
154
158
  /** Encodes an action passed back to the server as an argument. */
155
159
  function encodeFunction(value) {
156
160
  const reference = state().info.get(value);
@@ -252,7 +256,28 @@ function createReference(reference) {
252
256
  reference.redirect = void 0;
253
257
  return call(reference, args);
254
258
  };
255
- const result = client.queue.then(run).then((value) => {
259
+ const previous = client.queue;
260
+ const queued = () => previous.then(run);
261
+ const interceptor = client.interceptor;
262
+ let sent;
263
+ if (!interceptor) sent = queued();
264
+ else {
265
+ let stale = false;
266
+ const next = () => queued().then((value) => {
267
+ if (value === STALE) stale = true;
268
+ return value;
269
+ });
270
+ try {
271
+ sent = Promise.resolve(interceptor({
272
+ id,
273
+ name,
274
+ args: [...reference.bound, ...args]
275
+ }, next)).then((value) => stale ? STALE : value);
276
+ } catch (error) {
277
+ sent = Promise.reject(error);
278
+ }
279
+ }
280
+ const result = sent.then((value) => {
256
281
  if (value !== STALE) {
257
282
  const { redirect } = reference;
258
283
  emit("actionEnd", {
@@ -303,4 +328,4 @@ function createServerReference(id, token, name) {
303
328
  }
304
329
  return reference;
305
330
  }
306
- export { emit as a, createValueStore as c, defineClientHooks as i, getValueStore as l, decodeActionReference as n, hookEventName as o, setActionNavigator as r, setClientHooks as s, createServerReference as t, valueHeaders as u };
331
+ export { defineClientHooks as a, setClientHooks as c, valueHeaders as d, setActionNavigator as i, createValueStore as l, decodeActionReference as n, emit as o, setActionInterceptor as r, hookEventName as s, createServerReference as t, getValueStore as u };
@@ -1,6 +1,6 @@
1
1
  import { i as decodeTagged, n as NOT_A_VALUE, o as encodeLeaf } from "./codec-C8to-RQW.mjs";
2
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
- import { l as getValueStore, t as createServerReference } from "./actions-8RQfrM-p.mjs";
3
+ import { t as createServerReference, u as getValueStore } from "./actions-C-IwUCKU.mjs";
4
4
  import { a as getIslandServerHook, o as isolateIslands, r as getClientReference } from "./island-DVOr-OUw.mjs";
5
5
  import * as React from "react";
6
6
  // src/react/tree.ts
@@ -177,6 +177,7 @@ function transformClientModule(options) {
177
177
  const RUNTIME_ID = "virtual:islx/runtime";
178
178
  /** @internal The browser entry, for a framework whose own entry imports it. */
179
179
  const ENTRY_ID = "virtual:islx/client-entry";
180
+ const LIBRARY_RUNTIME_ID = "virtual:islx/react-for-libraries";
180
181
  const CLIENT_HOOKS_ID = "virtual:islx/hooks-client";
181
182
  const SERVER_HOOKS_ID = "virtual:islx/hooks-server";
182
183
  const BROWSER_GLOBALS = /* @__PURE__ */ new Set([
@@ -366,6 +367,7 @@ function islx(options = {}, host = {}) {
366
367
  preamble = resolved.plugins.some((plugin) => plugin.name === "vite:react-virtual-preamble");
367
368
  },
368
369
  resolveId(id) {
370
+ if (id === LIBRARY_RUNTIME_ID) return this.resolve("@farm.js/islx/react", path.join(config.root, "package.json"), { skipSelf: true });
369
371
  if (id === RUNTIME_ID || id === "virtual:islx/client-entry" || id === CLIENT_HOOKS_ID || id === SERVER_HOOKS_ID) return `\0${id}`;
370
372
  },
371
373
  async load(id) {
@@ -425,7 +427,8 @@ function islx(options = {}, host = {}) {
425
427
  return JSON.stringify(relative);
426
428
  },
427
429
  sideEffects: [RUNTIME_ID],
428
- directive: clientDirective
430
+ directive: clientDirective,
431
+ ...library ? { runtime: LIBRARY_RUNTIME_ID } : {}
429
432
  });
430
433
  if (island) {
431
434
  if (!library) transformActions({
package/dist/client.d.mts CHANGED
@@ -10,8 +10,24 @@ interface ActionNavigator {
10
10
  /** A client navigation to `href`, as the action's redirect. */
11
11
  navigate?(href: string): Promise<void> | void;
12
12
  }
13
+ /** A call of a server action, as an interceptor sees it. */
14
+ interface ActionCall {
15
+ id: string;
16
+ /** The export (or inline function) name, when known. */
17
+ name?: string;
18
+ /** The arguments, after any bound with `.bind`. */
19
+ args: readonly unknown[];
20
+ }
21
+ /**
22
+ * Wraps every server action call: call `next` to make the request, and return what the caller
23
+ * gets. A framework uses it to unwrap what its own server side adds to a result (cache
24
+ * invalidations, a query's cache entry) or to turn a returned error back into one it throws.
25
+ */
26
+ type ActionInterceptor = (call: ActionCall, next: () => Promise<unknown>) => Promise<unknown>;
13
27
  /** Connects actions to the framework's router. */
14
28
  declare function setActionNavigator(navigator: ActionNavigator): void;
29
+ /** Installs the framework's interceptor for server action calls (one; a later call replaces it). */
30
+ declare function setActionInterceptor(interceptor: ActionInterceptor | undefined): void;
15
31
  /** Decodes an action in a result or props: `{"$":"a","v":id,"b"?:sealed}`. */
16
32
  declare function decodeActionReference(object: Record<string, unknown>): AnyFunction | undefined;
17
33
  /** The function that calls a server action; one per id and sealed values, so props keep identity. */
@@ -55,4 +71,4 @@ declare const INTERACTION_EVENT = "f:island-interaction";
55
71
  declare const REPLAYABLE_SELECTOR = "button,[role=\"button\"],input[type=\"button\"],input[type=\"submit\"],input[type=\"reset\"]";
56
72
  /** The inline script's source (no `<script>` tag): pass it to the page's head or bootstrap. */
57
73
  declare const PREHYDRATION_SCRIPT: string;
58
- export { type ActionNavigator, type ClientHookEvent, type ClientHookEvents, type ClientHooks, INTERACTION_EVENT, NAVIGATION_RENDER, type NavigateOptions, type NavigationOptions, PENDING_INTERACTIONS, PREHYDRATION_SCRIPT, REPLAYABLE_SELECTOR, type ValueStore, createServerReference, createValueStore, decodeActionReference, defineClientHooks, enhanceForms, getValueStore, hookEventName, navigate, setActionNavigator, setClientHooks, startNavigation, valueHeaders };
74
+ export { type ActionCall, type ActionInterceptor, type ActionNavigator, type ClientHookEvent, type ClientHookEvents, type ClientHooks, INTERACTION_EVENT, NAVIGATION_RENDER, type NavigateOptions, type NavigationOptions, PENDING_INTERACTIONS, PREHYDRATION_SCRIPT, REPLAYABLE_SELECTOR, type ValueStore, createServerReference, createValueStore, decodeActionReference, defineClientHooks, enhanceForms, getValueStore, hookEventName, navigate, setActionInterceptor, setActionNavigator, setClientHooks, startNavigation, valueHeaders };
package/dist/client.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as emit, c as createValueStore, i as defineClientHooks, l as getValueStore, n as decodeActionReference, o as hookEventName, r as setActionNavigator, s as setClientHooks, t as createServerReference, u as valueHeaders } from "./_chunks/actions-8RQfrM-p.mjs";
1
+ import { a as defineClientHooks, c as setClientHooks, d as valueHeaders, i as setActionNavigator, l as createValueStore, n as decodeActionReference, o as emit, r as setActionInterceptor, s as hookEventName, t as createServerReference, u as getValueStore } from "./_chunks/actions-C-IwUCKU.mjs";
2
2
  import { C as RENDER_HEADER, I as readPostedAction, L as stripRuntimeFields } from "./_chunks/wire-QEQWBr5s.mjs";
3
3
  import { i as REPLAYABLE_SELECTOR, n as PENDING_INTERACTIONS, r as PREHYDRATION_SCRIPT, t as INTERACTION_EVENT } from "./_chunks/prehydration-B1GLTLdk.mjs";
4
4
  // src/client/forms.ts
@@ -194,4 +194,4 @@ function navigate(href, options = {}) {
194
194
  else location.assign(href);
195
195
  return Promise.resolve();
196
196
  }
197
- export { INTERACTION_EVENT, NAVIGATION_RENDER, PENDING_INTERACTIONS, PREHYDRATION_SCRIPT, REPLAYABLE_SELECTOR, createServerReference, createValueStore, decodeActionReference, defineClientHooks, enhanceForms, getValueStore, hookEventName, navigate, setActionNavigator, setClientHooks, startNavigation, valueHeaders };
197
+ export { INTERACTION_EVENT, NAVIGATION_RENDER, PENDING_INTERACTIONS, PREHYDRATION_SCRIPT, REPLAYABLE_SELECTOR, createServerReference, createValueStore, decodeActionReference, defineClientHooks, enhanceForms, getValueStore, hookEventName, navigate, setActionInterceptor, setActionNavigator, setClientHooks, startNavigation, valueHeaders };
@@ -1,8 +1,8 @@
1
- import { a as emit, l as getValueStore, t as createServerReference, u as valueHeaders } from "../_chunks/actions-8RQfrM-p.mjs";
1
+ import { d as valueHeaders, o as emit, t as createServerReference, u as getValueStore } from "../_chunks/actions-C-IwUCKU.mjs";
2
2
  import { i as decodeTagged, n as NOT_A_VALUE, r as decode, u as setOwn } from "../_chunks/codec-C8to-RQW.mjs";
3
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
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";
5
+ import { i as createServerTreeLayout, n as PAYLOAD_ONLY_ATTRIBUTE, o as updateServerTrees, r as createServerTreeElement } from "../_chunks/tree-C47p6khN.mjs";
6
6
  import { n as isIslandStrategy, r as scheduleHydration, t as ISLAND_STRATEGIES } from "../_chunks/schedule-CjoybS3b.mjs";
7
7
  import * as React from "react";
8
8
  import { hydrateRoot } from "react-dom/client";
@@ -5,7 +5,7 @@ import { i as encodeActionReference } from "../_chunks/actions-BuS5jDi0.mjs";
5
5
  import { t as getBuildDefaults } from "../_chunks/build-COIcs7wm.mjs";
6
6
  import { a as parseHeldValues, n as addressValues, o as selectValuesToSend, r as escapeScriptJson } from "../_chunks/values-37UIzxM0.mjs";
7
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";
8
+ import { a as renderServerTree, n as PAYLOAD_ONLY_ATTRIBUTE, t as Outlet } from "../_chunks/tree-C47p6khN.mjs";
9
9
  import * as React from "react";
10
10
  // src/react/server.ts
11
11
  /**
package/dist/react.mjs CHANGED
@@ -1,3 +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";
2
+ import { a as renderServerTree, t as Outlet } from "./_chunks/tree-C47p6khN.mjs";
3
3
  export { IslandsProvider, Outlet, createIsland, isolateIslands, renderServerTree, withinClientRoot };
@@ -1,4 +1,4 @@
1
- import { n as islx, t as ENTRY_ID } from "./_chunks/vite-DG7hBn8A.mjs";
1
+ import { n as islx, t as ENTRY_ID } from "./_chunks/vite-BTzYlEiY.mjs";
2
2
  import { normalizePath } from "vite";
3
3
  // src/tanstack-start/index.ts
4
4
  /**
@@ -30,7 +30,9 @@ export default createServerEntry({
30
30
  });
31
31
  `;
32
32
  const routerWrapper = (routerPath) => `import * as app from ${JSON.stringify(routerPath)};
33
+ import { PREHYDRATION_SCRIPT } from "@farm.js/islx/client";
33
34
  export * from ${JSON.stringify(routerPath)};
35
+ const islxHead = new WeakSet();
34
36
  export async function getRouter(...args) {
35
37
  const router = await app.getRouter(...args);
36
38
  const lifecycle = (router.serverSsrLifecycle ??= {});
@@ -38,6 +40,16 @@ export async function getRouter(...args) {
38
40
  (lifecycle.onServerSsrAttach ??= []).push((ssr) => {
39
41
  ssr.dehydrate = async () => ssr.disableHydration();
40
42
  });
43
+ const root = router.routeTree;
44
+ if (root && !islxHead.has(root)) {
45
+ islxHead.add(root);
46
+ const head = root.options.head;
47
+ const add = (own) => ({ ...own, scripts: [{ children: PREHYDRATION_SCRIPT }, ...(own?.scripts ?? [])] });
48
+ root.options.head = (context) => {
49
+ const own = head?.(context);
50
+ return own && typeof own.then === "function" ? own.then(add) : add(own);
51
+ };
52
+ }
41
53
  return router;
42
54
  }
43
55
  `;
package/dist/vite.mjs CHANGED
@@ -1 +1 @@
1
- export { t as ENTRY_ID, n as default, n as islx, r as resolveDirectives, i as serializeHooks } from "./_chunks/vite-DG7hBn8A.mjs";
1
+ export { t as ENTRY_ID, n as default, n as islx, r as resolveDirectives, i as serializeHooks } from "./_chunks/vite-BTzYlEiY.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/islx",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
4
4
  "description": "A renderer-agnostic wire format for server-rendered islands: typed props, values sent once, server actions, and partial navigations.",
5
5
  "keywords": [
6
6
  "islands",