@farm.js/islx 0.0.0-stage → 0.0.2

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/LICENSE +21 -0
  3. package/README.md +198 -2
  4. package/SPEC.md +220 -0
  5. package/dist/_chunks/actions-2sTj0A2T.mjs +306 -0
  6. package/dist/_chunks/actions-B8dJ8f7L.mjs +615 -0
  7. package/dist/_chunks/actions-BCtIGvrP.d.mts +58 -0
  8. package/dist/_chunks/actions-Eutk6UzT.mjs +326 -0
  9. package/dist/_chunks/build-COIcs7wm.mjs +9 -0
  10. package/dist/_chunks/codec-CK8kZp4u.d.mts +64 -0
  11. package/dist/_chunks/codec-DqfVYKsx.mjs +279 -0
  12. package/dist/_chunks/handle-C0N9ZyNV.d.mts +35 -0
  13. package/dist/_chunks/hooks-3VZV8f_d.d.mts +30 -0
  14. package/dist/_chunks/hooks-De0gNpOn.d.mts +64 -0
  15. package/dist/_chunks/prehydration-B1GLTLdk.mjs +36 -0
  16. package/dist/_chunks/schedule-BFoGKRrU.mjs +216 -0
  17. package/dist/_chunks/schedule-Dw14dW0H.d.mts +17 -0
  18. package/dist/_chunks/tree-BDB5VEao.mjs +777 -0
  19. package/dist/_chunks/tree-fID6sz25.d.mts +102 -0
  20. package/dist/_chunks/value-store-Dm-7xJ6W.d.mts +19 -0
  21. package/dist/_chunks/wire-C3Rpbwl6.d.mts +98 -0
  22. package/dist/_chunks/wire-CXOodE06.mjs +121 -0
  23. package/dist/client.d.mts +58 -0
  24. package/dist/client.mjs +197 -0
  25. package/dist/compiler.d.mts +2 -0
  26. package/dist/compiler.mjs +1 -0
  27. package/dist/index.d.mts +3 -0
  28. package/dist/index.mjs +3 -0
  29. package/dist/react/client.d.mts +49 -0
  30. package/dist/react/client.mjs +456 -0
  31. package/dist/react/server.d.mts +51 -0
  32. package/dist/react/server.mjs +84 -0
  33. package/dist/react.d.mts +3 -0
  34. package/dist/react.mjs +1 -0
  35. package/dist/server.d.mts +200 -0
  36. package/dist/server.mjs +378 -0
  37. package/dist/vite.d.mts +57 -0
  38. package/dist/vite.mjs +497 -0
  39. package/package.json +121 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ # Changelog
2
+
3
+ ## 0.0.2
4
+
5
+ Streaming and hydration fixes.
6
+
7
+ - Islands inside Suspense content that streams in after the browser entry has run now hydrate.
8
+ While the document streams, the runtime watches it: an island hydrates once React moves it out
9
+ of its hidden staging `<div>`, an island React removes (a replaced fallback) is released, and
10
+ watching stops once the document is parsed and every staged boundary is revealed. React 19.2
11
+ can reveal boundaries after parsing ends.
12
+ - Islands that share a value decode it once and share the result. Props and values without tags
13
+ skip decoding, and decoded objects are built with plain assignment (`__proto__` stays data).
14
+
15
+ ## 0.0.1
16
+
17
+ The first release: the islx wire format, server actions, React islands, and the Vite plugin.
18
+
19
+ ### Wire format (`@farm.js/islx`)
20
+
21
+ - `f-` vocabulary: `<f-island>`, `f/props`, `f/value`, `<f-slot>`, `f/tree`, `f/chunk`, `<f-html>`,
22
+ `f/form-state`; headers `F-Values`, `F-Layouts`, `F-Render`, `F-Action`.
23
+ - One reserved JSON key, `$`, with short tags: `Date`, `Map`, `Set`, `BigInt`, `URL`, `RegExp`,
24
+ bytes, `undefined`, special numbers, `FormData` and file arguments; app data with a `$` key
25
+ travels wrapped. Decoding refuses unknown tags and keeps `__proto__` an ordinary key.
26
+
27
+ ### Server (`@farm.js/islx/server`)
28
+
29
+ - Values sent once: large props keyed by a hash of their JSON, left out for browsers that hold
30
+ them.
31
+ - Server actions: `"use server"` modules and inline `"use server"` functions. Captured variables
32
+ and `.bind` arguments travel sealed (AES-GCM, bound to the action, key rotation).
33
+ - `handleAction(request)`: calls and forms without JavaScript, `<button formAction>`,
34
+ `useActionState` form state, origin checks, a body limit, stale-deploy answers, errors that
35
+ never reach the browser.
36
+ - Lifecycle hooks: `defineServerHooks({ action, actionError, error })`.
37
+
38
+ ### Browser (`@farm.js/islx/client`)
39
+
40
+ - Action references: calls in order, refresh after each, redirects, stale-deploy reloads.
41
+ - Client navigation, view transitions, server-component forms without reloads.
42
+ - Lifecycle hooks and `f:` DOM events; the pre-hydration script that holds early interactions.
43
+
44
+ ### React (`@farm.js/islx/react`, `/react/server`, `/react/client`)
45
+
46
+ - `createIsland`, `renderToStream`, the hydration runtime (`load`, `idle`, `visible`,
47
+ `interaction`), carry-over across refreshes, server HTML restored on failure.
48
+ - Server trees for layouts that hydrate as one root, payload-only navigations.
49
+ - For frameworks: `IslandsProvider`, `withinClientRoot`, `isolateIslands`, `createServerHtml`.
50
+
51
+ ### Build (`@farm.js/islx/compiler`, `@farm.js/islx/vite`)
52
+
53
+ - `transformActions` and `transformClientModule`, with errors that say where and what to do.
54
+ - `islx()` for Vite 6+: discovery, one chunk per client module, preloads, dev reloads,
55
+ `directives`, `hooks`, `navigation`, `clientComponents`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Farming Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,199 @@
1
- # Temporary Holding Version
1
+ # islx
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A wire format for server-rendered islands. Pages are plain server HTML; the interactive parts
4
+ hydrate on their own, with typed props, values sent once, server actions, and navigations that
5
+ send only what changed.
6
+
7
+ ```html
8
+ <f-island src="/src/cart.tsx" export="Cart" props="_R_1_" strategy="load">…server HTML…</f-island>
9
+ <script type="f/props" for="_R_1_">{"updated":{"$":"D","v":"2026-10-28T09:00:00.000Z"}}</script>
10
+ ```
11
+
12
+ islx started as [Farm.js](https://github.com/farming-labs/farm.js)'s islands mode, where it
13
+ replaces React Server Components' Flight payload. On a 300-page docs app it ships 26% less
14
+ JavaScript, and a client navigation sends 2.5 KB instead of 12 KB.
15
+
16
+ > Status: 0.0.2. React and Vite are supported; other renderers and framework
17
+ > integrations follow. The wire format is in [SPEC.md](./SPEC.md), changes in
18
+ > [CHANGELOG.md](./CHANGELOG.md).
19
+
20
+ ```bash
21
+ npm install @farm.js/islx
22
+ ```
23
+
24
+ ## What is here
25
+
26
+ ```ts
27
+ import { encode, decode } from "@farm.js/islx";
28
+ import { addressValues, selectValuesToSend, handleAction } from "@farm.js/islx/server";
29
+ import { getValueStore, createServerReference, setActionNavigator } from "@farm.js/islx/client";
30
+ import { transformActions } from "@farm.js/islx/compiler";
31
+ ```
32
+
33
+ - **Typed values.** `Date`, `Map`, `Set`, `BigInt`, `URL`, `RegExp`, bytes, `undefined` and
34
+ special numbers survive the trip, in props and in action arguments and results.
35
+ - **Values sent once.** Large props are keyed by a hash of their JSON. Islands and pages that
36
+ share one share its key, and a navigation leaves out what the browser holds.
37
+ - **Server actions.** `"use server"` modules and inline `"use server"` functions, with:
38
+ - the variables an inline action reads from its component carried along, sealed (AES-GCM,
39
+ bound to the action) so the browser can neither read nor change them;
40
+ - forms that work before JavaScript loads, including `<button formAction>` and
41
+ `useActionState`;
42
+ - `.bind` on the server (sealed) and in the browser;
43
+ - calls run in order, then a page refresh; a redirect navigates; an action from an older
44
+ deploy reloads the page;
45
+ - same-origin checks, a body size limit, and errors that never reach the browser.
46
+ - **React islands.** `createIsland` renders a client component as server HTML plus its props;
47
+ `renderToStream` renders a page (leaving out values the browser holds); the hydration runtime
48
+ starts each island as its own root when its strategy says (`load`, `idle`, `visible`,
49
+ `interaction`, which holds the first click or submit and replays it), restores the server HTML
50
+ if an island fails, keeps island state when the page renders again, and hydrates
51
+ `useActionState` with the form post's result.
52
+ - **Compiler.** A pure function from source to source for each environment, with errors that
53
+ say where and what to do: an inline action in browser code, one that is not async, uses
54
+ `this`, or captures something that cannot travel.
55
+
56
+ ### Server actions in a framework
57
+
58
+ ```ts
59
+ // The server: before rendering a page.
60
+ const response = await handleAction(request, {
61
+ allowedOrigins: ["app.example.com"],
62
+ mapError: (error) => (isRedirect(error) ? { redirect: error.location } : undefined),
63
+ renderFormState: (formState) => renderPage(request, { formState }),
64
+ });
65
+ if (response) return response;
66
+
67
+ // The browser: once.
68
+ setActionNavigator({ refresh: () => router.refresh(), navigate: (href) => router.push(href) });
69
+ ```
70
+
71
+ Every server instance must share the key captured values are sealed with: set
72
+ `ISLX_ACTIONS_KEY` (make one with `generateActionKey()`), or call `configureActionKeys`. Keep a
73
+ rotated-out key in `ISLX_ACTIONS_PREVIOUS_KEYS` so open pages keep working.
74
+
75
+ ### Known limits
76
+
77
+ - Every registered action is a public endpoint, as in React and Next.js: check inputs and rights
78
+ inside it.
79
+ - Arguments carry the typed values above, `FormData`, `File` and `Blob`.
80
+ - Actions are not removed from the server build when nothing imports them yet.
81
+
82
+ ## On Vite
83
+
84
+ ```ts
85
+ // vite.config.ts
86
+ import react from "@vitejs/plugin-react";
87
+ import { islx } from "@farm.js/islx/vite";
88
+
89
+ export default defineConfig({
90
+ plugins: [react(), islx()],
91
+ environments: {
92
+ ssr: { build: { outDir: "dist/server", rollupOptions: { input: "src/entry-server.tsx" } } },
93
+ },
94
+ });
95
+ ```
96
+
97
+ ```tsx
98
+ // src/entry-server.tsx
99
+ import { renderToStream } from "@farm.js/islx/react/server";
100
+ import { handleAction } from "@farm.js/islx/server";
101
+
102
+ export async function handle(request: Request) {
103
+ const action = await handleAction(request, {
104
+ renderFormState: (formState) => page(request, formState),
105
+ });
106
+ return action ?? page(request);
107
+ }
108
+
109
+ async function page(request: Request, formState?) {
110
+ const stream = await renderToStream(<App />, { request, formState });
111
+ return new Response(stream, { headers: { "content-type": "text/html", "cache-control": "private" } });
112
+ }
113
+ ```
114
+
115
+ ```tsx
116
+ // src/components/chart.tsx
117
+ "use client";
118
+ export const hydrate = "visible"; // or { Chart: "visible", Legend: "idle" }; default "load"
119
+ export function Chart(props: { points: Map<string, number> }) { … }
120
+ ```
121
+
122
+ - Components are server components unless their module says `"use client"`; those render as
123
+ islands, and the page needs no other setup: `renderToStream` adds the browser entry.
124
+ - `vite build` builds the server, then the browser with exactly the client components the server
125
+ rendered, each in its own chunk. `islx({ clientComponents: { include, exclude } })` adds
126
+ components the server build cannot see (chosen by name at runtime) or leaves some out.
127
+ - Forms in server components submit without a reload once JavaScript runs, and post normally
128
+ before.
129
+ - Same-site links move between pages without loading a new document: the runtime, its code and
130
+ the values the browser holds stay, and the server leaves those values out. Back, forward,
131
+ scroll and focus behave as page loads do. `islx({ navigation: false })` turns it off;
132
+ `rel="external"` opts one link out; `navigate(href)` from `@farm.js/islx/client` navigates from
133
+ code.
134
+ - `islx({ directives: { client: "use island", server: "use action" } })` renames the directives
135
+ your code uses, for apps where another tool already owns `"use client"` or `"use server"`.
136
+ Libraries keep `"use client"`: their components become islands when Vite bundles them for the
137
+ server (`ssr.noExternal`), and their `"use server"` never registers an action.
138
+ - Lifecycle hooks go in the plugin's options:
139
+
140
+ ```ts
141
+ islx({
142
+ navigation: { viewTransition: true },
143
+ hooks: {
144
+ client: {
145
+ navigate(event) { if (window.unsaved) event.preventDefault(); }, // stay on the page
146
+ navigated({ url, duration }) { window.plausible?.("pageview", { u: url.href }); },
147
+ hydrate({ export: name, duration }) { console.debug(name, "interactive in", duration); },
148
+ islandError({ src, error }) { console.error(src, error); },
149
+ actionEnd({ name, ok, duration }) {},
150
+ },
151
+ server: {
152
+ async action({ request, name, module }, run) { /* check, log, time */ return run(); },
153
+ actionError: (error) => (error instanceof Unauthorized ? { status: 401 } : undefined),
154
+ error(error, { name }) {},
155
+ },
156
+ },
157
+ })
158
+ ```
159
+
160
+ Callbacks are copied into the browser or server build as source, so they may use only their
161
+ arguments and globals; the build names anything else they use. Hooks that import app code (an
162
+ auth helper, an error reporter) go in a module: `hooks: { server: "./src/islx.server.ts" }`,
163
+ exporting `defineServerHooks({ … })` (or `defineClientHooks`). Every client hook also fires as a
164
+ DOM event on `document`: `f:navigate`, `f:navigated`, `f:navigate-error`, `f:hydrate`,
165
+ `f:island-error`, `f:action`, `f:action-end`.
166
+ - Each build makes its own key for sealed action values; set `ISLX_ACTIONS_KEY` to keep forms
167
+ working across deploys.
168
+
169
+ `examples/react` is a runnable app; `pnpm test:e2e` runs it in Chrome, in development and from
170
+ its production build.
171
+
172
+ ## In a framework
173
+
174
+ A framework that renders pages itself (Farm does) uses the pieces directly instead of
175
+ `renderToStream` and the Vite plugin's entry:
176
+
177
+ | Need | API |
178
+ | --- | --- |
179
+ | Compile client and action modules | `transformClientModule({ …, reference, strategy })`, `transformActions` from `@farm.js/islx/compiler` |
180
+ | Render islands in its own render | wrap the page in `<IslandsProvider held={parseHeldValues(request)}>` |
181
+ | A layout that hydrates as one root | `withinClientRoot(layout)`; its server-HTML page: `isolateIslands(page)` |
182
+ | An async page under such a layout | `renderServerTree(page, { held, payloadOnly })` on the server; `createServerTreeElement(source, { load, ServerHtml })` in the browser |
183
+ | Hydrate | `createHydrationRuntime({ load, schedule?, wrap?, report? })`, `createServerHtml(runtime)` |
184
+ | Hold early clicks | inline `PREHYDRATION_SCRIPT` in the page |
185
+ | Actions | `handleAction(request, { allowedOrigins, bodySizeLimit, run, mapError, renderFormState })`; `setActionNavigator({ refresh, navigate })` |
186
+ | Navigation with its router | send `F-Values` (`valueHeaders()`), `F-Layouts`, `F-Render`; `refreshDocument` / `swapDocument` or its own swap |
187
+
188
+ ## Planned
189
+
190
+ | Entry | For |
191
+ | --- | --- |
192
+ | `@farm.js/islx/react` | server trees (a page rebuilt inside a hydrated layout), partial navigations |
193
+ | `@farm.js/islx/webpack` | webpack, Rspack, and Next.js (Turbopack uses the loader) |
194
+ | `@farm.js/islx/next`, `@farm.js/islx/astro`, `@farm.js/islx/nuxt` | thin framework integrations |
195
+ | `@farm.js/islx/testing` | the conformance suite every integration must pass |
196
+
197
+ ## License
198
+
199
+ MIT
package/SPEC.md ADDED
@@ -0,0 +1,220 @@
1
+ # islx wire format
2
+
3
+ Version 1. This document is the contract between a islx server and a islx browser runtime.
4
+ Anything that writes and reads it, in any language or framework, is a islx implementation.
5
+
6
+ ## Principles
7
+
8
+ - **HTML first.** A page is ordinary server HTML. Interactive parts are marked where they sit;
9
+ nothing else is reserved.
10
+ - **One prefix.** Every element, script type and header islx owns starts with `f`. Custom
11
+ elements need a hyphen (`f-island`), script types use a slash (`f/props`), headers start `F-`.
12
+ - **One reserved JSON key.** Inside JSON, `$` holds a short tag. App data that has its own `$`
13
+ key is wrapped, so no key is off limits to apps.
14
+ - **Untrusted by default.** Everything the browser sends back (headers, action arguments, sealed
15
+ values) is validated. References are never accepted from the browser unless the server checks
16
+ them.
17
+
18
+ ## HTML
19
+
20
+ ```html
21
+ <f-island src="/src/toc.tsx" export="Toc" props="_R_b_" strategy="load">
22
+ …the component's server HTML…
23
+ <f-slot name="_R_5_s0">…server output passed as children…</f-slot>
24
+ </f-island>
25
+ <script type="f/value" name="04jiw29097x8ki">[…a value sent once…]</script>
26
+ <script type="f/props" for="_R_b_">{"headings":[…],"tree":{"$":"r","v":"04jiw29097x8ki"}}</script>
27
+ ```
28
+
29
+ | Name | Meaning |
30
+ | --- | --- |
31
+ | `<f-island>` | A client component's server HTML. `src` names the module the browser imports (its path from the app root; the runtime maps it to a URL), `export` the component's export name, `props` the id of its props script, `strategy` when it hydrates: `load`, `idle`, `visible` or `interaction`. A hydrated island gets a `hydrated` attribute. |
32
+ | `<script type="f/props" for>` | An island's props, as JSON. `for` names the island's `props` id. |
33
+ | `<script type="f/value" name>` | A value sent once. `name` is its key. Written before the first island that uses it. |
34
+ | `<f-slot name>` | Server output passed to an island as children. It stays server HTML; the island renders it in place. |
35
+ | `<script type="f/tree" [partial]>` | A page's server tree, so the browser can rebuild the page inside an existing root without its HTML. `partial` marks a navigation that sent only the tree. |
36
+ | `<script type="f/chunk" name>` | A part of a server tree that streamed in after the tree (a resolved Suspense boundary). |
37
+ | `<f-html name>` | Server HTML inside a server tree. |
38
+
39
+ A value script's and props script's JSON escapes `<`, U+2028 and U+2029.
40
+
41
+ ## JSON tags
42
+
43
+ A tagged object is `{"$":tag}` or `{"$":tag,"v":data}`; an action may also carry `"b"`. An object
44
+ with any other keys is data.
45
+
46
+ | Tag | Stands for | Data |
47
+ | --- | --- | --- |
48
+ | `r` | a value sent once | its key |
49
+ | `s` | a slot | its name |
50
+ | `a` | a server action | its id; `b`, when present, holds its sealed captured values |
51
+ | `o` | app data with a `$` key | the object |
52
+ | `u` | `undefined` | |
53
+ | `n` | `NaN`, `Infinity`, `-Infinity`, `-0` | the name |
54
+ | `B` | `BigInt` | decimal digits |
55
+ | `D` | `Date` | ISO string, `null` for an invalid date |
56
+ | `M` | `Map` | `[[key, value], …]`, both tagged |
57
+ | `S` | `Set` | `[value, …]`, tagged |
58
+ | `U` | `URL` | `href` |
59
+ | `R` | `RegExp` | `[source, flags]` |
60
+ | `Y` | bytes | `[type, base64]`; type is `ArrayBuffer`, `DataView` or a typed array name |
61
+ | `p` | a Suspense boundary inside a server tree | |
62
+ | `F` | a `FormData` action argument | its index among the call's attachments |
63
+ | `K` | a `Blob` or `File` action argument | its index among the call's attachments |
64
+
65
+ The browser resolves `r` first (from its value store), then decodes the rest. Inside `o`, only
66
+ the object's properties are read; the object itself is never a tag. Unknown tags are errors.
67
+
68
+ Decoders build plain objects with own properties, so a `__proto__` key stays an ordinary key.
69
+
70
+ ## Values sent once
71
+
72
+ A value whose JSON reaches 512 characters travels in its own `f/value` script, keyed by a 64-bit
73
+ hash of its JSON (14 base-36 characters), and props refer to it as `{"$":"r","v":key}`. Inner
74
+ values are addressed first, so a value that changed in one place shares its unchanged parts. A
75
+ thin wrapper around one addressed value is not addressed.
76
+
77
+ The browser keeps every value it reads, up to 1024, and names the most recently used 128 in each
78
+ navigation:
79
+
80
+ ```http
81
+ GET /docs/p1
82
+ F-Values: 04jiw29097x8ki,1kq0d3f7xg2m9a
83
+ ```
84
+
85
+ The server leaves those values out. A response that left values out belongs to one browser: it
86
+ must never enter a shared cache (CDN, ISR entry, PPR shell), and a cacheable response sends
87
+ `Vary: F-Values`.
88
+
89
+ ## Navigations
90
+
91
+ | Header | Meaning |
92
+ | --- | --- |
93
+ | `F-Values` | value keys the browser holds |
94
+ | `F-Layouts` | the layouts on screen, as a JSON array of layout keys (pattern with params filled in) |
95
+ | `F-Render: tree` | the browser can rebuild the page from its server tree; HTML may be left out |
96
+ | `F-Render: document` | a navigation: the browser puts the whole page in place with DOM methods, so the server sends it in one piece (every Suspense boundary resolved, no streaming scripts) |
97
+
98
+ Without a framework router, the browser runtime navigates itself: a same-site link click fetches
99
+ the page with `F-Render: document` and `F-Values`, swaps the body in, starts the next page's
100
+ islands, and handles history, scroll and focus. Anything unusual loads the page normally (another
101
+ site, `target`, `download`, `rel="external"`, modifier keys, a non-HTML answer, or a page whose
102
+ browser entry differs, as after a deploy).
103
+
104
+ A server that leaves layouts out renders each kept layout as an empty boundary, and the browser
105
+ keeps its own. If the response does not fit what is on screen, the browser loads the full
106
+ document. All three headers make a response private, and the server sends `Vary` for each one
107
+ it read.
108
+
109
+ ## Server actions
110
+
111
+ An action id names one server function: a hash of its module's path from the app root and its
112
+ export name (`inline:N` for the Nth inline action in the module). Ids are opaque to the browser.
113
+ Every registered action is a public endpoint: anyone can call it with any arguments, so an
114
+ action checks its inputs and the caller's rights like any other request handler.
115
+
116
+ An action's **bound values** are the variables an inline action captured, then arguments added
117
+ with `.bind` on the server. They travel sealed (below) and come before the call's own arguments.
118
+
119
+ **Called from JavaScript:**
120
+
121
+ ```http
122
+ POST /current/page
123
+ F-Action: nSaHH8FImEr_M6
124
+ Content-Type: text/plain;charset=UTF-8
125
+
126
+ {"a":[…arguments, tagged…],"b":"…sealed…"}
127
+ ```
128
+
129
+ A single `FormData` argument is sent as `multipart/form-data` instead, with the sealed values in
130
+ the `$F_BOUND` field. Arguments that hold `FormData` or files anywhere (as `useActionState`'s
131
+ `(previousState, formData)`) are sent as multipart too: the JSON goes in the `$F_CALL` field,
132
+ attachment `n` is `{"$":"F","v":n}` or `{"$":"K","v":n}` in it, a file travels as field `$F_n`
133
+ and a `FormData`'s fields as `$F_n_<name>`. The response is JSON: `{"result":…tagged…}` or `{"redirect":"/path"}`.
134
+ Arguments may contain actions (`a`, with `b` when they carry sealed values) the server
135
+ registered; never `r` or `s`. The browser runs calls one at a time, in order, and refreshes the
136
+ page after each one (a redirect navigates instead).
137
+
138
+ **Posted by a form without JavaScript:** React writes the fields while it renders the form, for
139
+ `<form action>` and `<button formAction>`:
140
+
141
+ ```html
142
+ <form action="" method="POST" enctype="multipart/form-data">
143
+ <input type="hidden" name="$F_REF_R_1_">
144
+ <input type="hidden" name="$F_R_1_:A" value="…action id…">
145
+ <input type="hidden" name="$F_R_1_:B" value="…sealed…">
146
+ <button name="$F_REF_R_2_">Delete</button>
147
+ <input type="hidden" name="$F_R_2_:A" value="…another action…">
148
+ …
149
+ </form>
150
+ ```
151
+
152
+ The last `$F_REF_` field a submission carries names the action, so a clicked button wins over
153
+ its form. Every `$F_` field and `$ACTION_KEY` is removed before the action sees the form. The
154
+ server answers `303 See Other` back to the page, or to the action's redirect. A form posted from
155
+ a `useActionState` hook carries React's `$ACTION_KEY`; a server that can render the page renders
156
+ it with React's form state `[result, $ACTION_KEY, id, bound count - 1]` instead of redirecting,
157
+ and hydrates with the same.
158
+
159
+ **Checks the server makes**, before running anything:
160
+
161
+ - the method is `POST`, and the request names a same-origin source (`Origin`, then `Referer`,
162
+ then `Sec-Fetch-Site: same-origin`), or one from an origin the app allows;
163
+ - the body is within the size limit (`413`); a form over the limit that does not post an action
164
+ is left to the app;
165
+ - the id is registered; an unknown id is `404` with `F-Action-Stale: 1` for calls, and a `303`
166
+ back to the page for forms, so the browser loads the page this deploy renders;
167
+ - arguments decode; malformed or unknown tags are `400`;
168
+ - sealed values open for this id: values sealed with a key the server does not have are stale
169
+ (`409`, `F-Action-Stale: 1`), anything else that does not open is `400`, and so is an inline
170
+ action that captured values posted without them.
171
+
172
+ A failing action answers `500` with no details; the error is reported on the server.
173
+
174
+ **Forms in server components** belong to no island. Once the browser runtime runs, a submit of a
175
+ form that posts an action is sent as a call (same fields, same checks) and the page refreshes,
176
+ keeping island state; without JavaScript it posts as above.
177
+
178
+ **Form state after a post.** A page rendered with React's form state carries it for hydration:
179
+ `<script type="f/form-state">…tagged JSON…</script>`. Every island root hydrates with it.
180
+
181
+ ## Sealed captured values
182
+
183
+ An action written inside a component may read variables of the render around it. Those values
184
+ travel with the action, sealed, so the browser can neither read nor change them:
185
+
186
+ ```
187
+ sealed = base64url( version ‖ key id ‖ IV ‖ AES-256-GCM(ciphertext ‖ tag) )
188
+ ```
189
+
190
+ - `version` is `1`; `key id` is the first byte of SHA-256 of the key; the IV is 12 random bytes;
191
+ - the additional data is `islx:action:<id>`, so values sealed for one action never open for
192
+ another;
193
+ - the plaintext is the captured values as a tagged JSON array.
194
+
195
+ A value sealed with a key the server does not have is **stale** (the page reloads); anything that
196
+ does not open is **invalid** (`400`). Servers may hold previous keys, so pages rendered before a
197
+ key rotation keep working. Every server instance must share the keys.
198
+
199
+ ## Integration model
200
+
201
+ islx is layered so each framework needs only a thin integration:
202
+
203
+ | Layer | Package entry | Depends on |
204
+ | --- | --- | --- |
205
+ | Wire format and codec | `@farm.js/islx` | nothing (Web APIs) |
206
+ | Server: values, sealing, action handling | `@farm.js/islx/server` | `@farm.js/islx` |
207
+ | Browser: value store, hydration, navigation | `@farm.js/islx/client` | `@farm.js/islx` |
208
+ | Renderer adapters | `@farm.js/islx/react` (then Preact, Vue, Svelte, Solid) | the renderer |
209
+ | Compiler: `"use client"`, `"use server"`, inline actions | `@farm.js/islx/compiler` | a parser |
210
+ | Bundler plugins | `@farm.js/islx/vite`, `@farm.js/islx/webpack`, `@farm.js/islx/rollup`, `@farm.js/islx/esbuild` | the compiler |
211
+ | Frameworks | `@farm.js/islx/next`, `@farm.js/islx/astro`, `@farm.js/islx/nuxt`, … | the above |
212
+
213
+ - **Request handling** is one function on Web `Request` and `Response`, so any server mounts it:
214
+ a Next.js route handler or middleware, a Nitro event handler, Astro middleware, Hono, Express
215
+ through an adapter.
216
+ - **The compiler** is a pure function from source and environment (`server` or `client`) to
217
+ source, with no bundler in it. Each bundler plugin is a few lines around it, and Turbopack uses
218
+ the webpack loader.
219
+ - **Conformance.** `@farm.js/islx/testing` will ship the contract suite: an implementation passes when its
220
+ fixture app passes it, in development and production.