@farm.js/islx 0.0.8 → 0.0.9

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.9
4
+
5
+ Islands behave as React Server Components do in four cases they did not.
6
+
7
+ - `useId` in an island returns the ids its server HTML has, and two islands no longer share ids.
8
+ The props script carries the island's id position (`ids`), which the browser rebuilds before it
9
+ hydrates. The island wrapper no longer calls `useId` inside another island, where it moved every
10
+ id below it.
11
+ - Client components in server output passed to an island (`children`, or any element prop) are
12
+ the island's descendants: they read its context, as a consumer below a provider does, and keep
13
+ their own state. Such output travels as a server tree named after its slot; plain HTML still
14
+ travels as HTML only.
15
+ - An element prop the island does not render at first (a tab, a closed panel) renders when it
16
+ does. The server writes the slots it did not place in a `<template>` after the props.
17
+ - `useFormStatus` in a client component inside a server component's `<form>` reports that form's
18
+ submit. The compiler points `useFormStatus` from `react-dom` at `@farm.js/islx/react`'s, which
19
+ is React's anywhere else; `rewriteFormStatusImports` in `@farm.js/islx/compiler` does it for
20
+ other plugins. A namespace import (`ReactDOM.useFormStatus`) keeps React's.
21
+
3
22
  ## 0.0.8
4
23
 
5
24
  Pages load the runtime only when they first need it, and islands that only react to clicks wait
package/README.md CHANGED
@@ -15,7 +15,7 @@ islx started as [Farm.js](https://github.com/farming-labs/farm.js)'s islands mod
15
15
  replaces React Server Components' Flight payload. On a 300-page docs app it ships 26% less
16
16
  JavaScript, and a client navigation sends 2.5 KB instead of 12 KB.
17
17
 
18
- > Status: 0.0.8. React, Vite and TanStack Start are supported; other renderers and framework
18
+ > Status: 0.0.9. React, Vite and TanStack Start are supported; other renderers and framework
19
19
  > integrations follow. The wire format is in [SPEC.md](./SPEC.md), changes in
20
20
  > [CHANGELOG.md](./CHANGELOG.md).
21
21
 
package/SPEC.md CHANGED
@@ -29,9 +29,10 @@ Anything that writes and reads it, in any language or framework, is a islx imple
29
29
  | Name | Meaning |
30
30
  | --- | --- |
31
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. |
32
+ | `<script type="f/props" for [ids]>` | An island's props, as JSON. `for` names the island's `props` id. `ids` is the `useId` React gave a probe the server renders first in the island, at index 0 of a two-child array whose second child is the component: its bits and length are the island's id position, which the browser rebuilds before it hydrates the component at index 1, so the component's `useId` calls match its HTML. Without it the browser hydrates with React's own ids. |
33
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. |
34
+ | `<f-slot name>` | Server output passed to an island as children (or any element prop). Plain HTML stays server HTML, and the island renders it in place. Output holding client components, Suspense boundaries, actions or `<f-html>` parts carries a server tree (`<script type="f/tree" name>` named after the slot, inside it), which the browser rebuilds inside the island's root: its client components are the island's descendants and read its context. |
35
+ | `<template for>` | After an island's props: the `<f-slot>`s the island did not place when the server rendered it (a closed panel, a tab not shown yet). `for` names the island's `props` id. The browser reads a slot from here when the island places it. |
35
36
  | `<script type="f/tree" [name] [partial]>` | A server tree, so the browser can rebuild the page inside an existing root without its HTML. A page's tree has no `name`; a layout's carries its scope and sits at its `children` outlet, or after the layout when it has none. `partial` marks a navigation that sent only the tree. |
36
37
  | `<script type="f/chunk" name>` | A part of a server tree that streamed in after the tree (a resolved Suspense boundary). `name` is its number, after `scope:` in a layout's tree. |
37
38
  | `<f-html name>` | Server HTML inside a server tree, named like a chunk. |
@@ -41,8 +42,8 @@ A value script's, props script's and promise script's JSON escapes `<`, U+2028 a
41
42
 
42
43
  ## JSON tags
43
44
 
44
- A tagged object is `{"$":tag}` or `{"$":tag,"v":data}`; an action may also carry `"b"`. An object
45
- with any other keys is data.
45
+ A tagged object is `{"$":tag}` or `{"$":tag,"v":data}`; an action may also carry `"b"`, and a slot
46
+ `"t":1` when it carries a server tree. An object with any other keys is data.
46
47
 
47
48
  | Tag | Stands for | Data |
48
49
  | --- | --- | --- |
@@ -1,5 +1,5 @@
1
- import { A as VALUES_HEADER, F as getTag, M as VALUE_SCRIPT_TYPE, N as attachmentField, j as VALUE_KEY_PATTERN, o as BOUND_FIELD, s as CALL_FIELD, t as ACTION_HEADER } from "./wire-QEQWBr5s.mjs";
2
- import { a as encode, r as decode, u as setOwn } from "./codec-C8to-RQW.mjs";
1
+ import { F as VALUE_SCRIPT_TYPE, I as attachmentField, N as VALUES_HEADER, P as VALUE_KEY_PATTERN, R as getTag, o as BOUND_FIELD, s as CALL_FIELD, t as ACTION_HEADER } from "./wire-UUwZlUJZ.mjs";
2
+ import { a as encode, r as decode, u as setOwn } from "./codec-DDpUvgRo.mjs";
3
3
  // src/client/value-store.ts
4
4
  /**
5
5
  * The browser's store of values sent once. It keeps every value it has read, parses each once,
@@ -1,6 +1,6 @@
1
- import { P as formFieldName, d as FORM_REF_PREFIX } from "./wire-QEQWBr5s.mjs";
1
+ import { L as formFieldName, d as FORM_REF_PREFIX } from "./wire-UUwZlUJZ.mjs";
2
2
  import { t as getBuildDefaults } from "./build-COIcs7wm.mjs";
3
- import { a as encode, r as decode, t as EncodeError } from "./codec-C8to-RQW.mjs";
3
+ import { a as encode, r as decode, t as EncodeError } from "./codec-DDpUvgRo.mjs";
4
4
  // src/server/seal.ts
5
5
  /**
6
6
  * Sealing an action's captured values. An inline `"use server"` function that reads a variable of
@@ -1,4 +1,4 @@
1
- import { F as getTag } from "./wire-QEQWBr5s.mjs";
1
+ import { R as getTag } from "./wire-UUwZlUJZ.mjs";
2
2
  // src/protocol/codec.ts
3
3
  /**
4
4
  * Values JSON cannot hold. Each travels as a tagged object: the reserved key `$` holds a short tag
@@ -591,4 +591,49 @@ function findProblem(node) {
591
591
  }
592
592
  return null;
593
593
  }
594
- export { transformActions as a, hasModuleDirective as i, ActionCompileError as n, getActionId as r, inferStrategy as t };
594
+ // src/compiler/form-status.ts
595
+ /**
596
+ * Points a client module's `useFormStatus` import from `react-dom` at islx's hook, which also
597
+ * reports a server component's form around an island (`@farm.js/islx/react` `useFormStatus`).
598
+ * Both builds rewrite it, so the server and the browser render the same hook. A namespace import
599
+ * (`ReactDOM.useFormStatus()`) keeps React's.
600
+ */
601
+ const HOOK = "useFormStatus";
602
+ function importedName(specifier) {
603
+ const imported = specifier.imported;
604
+ return imported?.type === "Identifier" ? imported.name : imported?.value;
605
+ }
606
+ /** Applies the rewrite to `output`; true when the module imported the hook. */
607
+ function rewriteFormStatusImport(program, code, output, from) {
608
+ let changed = false;
609
+ for (const node of program.body ?? []) {
610
+ if (node.type !== "ImportDeclaration" || node.source?.value !== "react-dom") continue;
611
+ if (node.importKind === "type") continue;
612
+ const specifiers = node.specifiers ?? [];
613
+ const hooks = specifiers.filter((specifier) => specifier.type === "ImportSpecifier" && specifier.importKind !== "type" && importedName(specifier) === HOOK);
614
+ if (hooks.length === 0) continue;
615
+ const others = specifiers.filter((specifier) => !hooks.includes(specifier));
616
+ const defaults = others.filter((specifier) => specifier.type !== "ImportSpecifier");
617
+ const named = others.filter((specifier) => specifier.type === "ImportSpecifier");
618
+ const text = (specifier) => code.slice(specifier.start, specifier.end);
619
+ const clause = [...defaults.map(text), ...named.length > 0 ? [`{ ${named.map(text).join(", ")} }`] : []].join(", ");
620
+ const lines = [...clause ? [`import ${clause} from "react-dom";`] : [], `import { ${hooks.map((hook) => `${HOOK} as ${hook.local.name}`).join(", ")} } from ${JSON.stringify(from)};`];
621
+ output.overwrite(node.start, node.end, lines.join("\n"));
622
+ changed = true;
623
+ }
624
+ return changed;
625
+ }
626
+ /** The rewrite for a framework's own transform: the module's new code and map, or null. */
627
+ function rewriteFormStatusImports(options) {
628
+ if (!options.code.includes(HOOK)) return null;
629
+ const output = new MagicString(options.code);
630
+ if (!rewriteFormStatusImport(options.parse(options.code), options.code, output, options.from ?? "@farm.js/islx/react")) return null;
631
+ return {
632
+ code: output.toString(),
633
+ map: output.generateMap({
634
+ source: options.id,
635
+ hires: true
636
+ })
637
+ };
638
+ }
639
+ export { getActionId as a, ActionCompileError as i, rewriteFormStatusImports as n, hasModuleDirective as o, inferStrategy as r, transformActions as s, rewriteFormStatusImport as t };
@@ -0,0 +1,63 @@
1
+ import { B as stripRuntimeFields, z as readPostedAction } from "./wire-UUwZlUJZ.mjs";
2
+ import { t as createServerReference } from "./actions-DAzd4PpV.mjs";
3
+ // src/client/forms.ts
4
+ /**
5
+ * Forms written in server components submit without reloading the page once JavaScript runs.
6
+ * No island owns them, so nothing else would: a submit of a form that posts an action is sent as
7
+ * a call, and the page refreshes as after any action, keeping island state. Forms inside islands
8
+ * are React's, which handles them first.
9
+ */
10
+ let enhanced = false;
11
+ /** No submission in flight. */
12
+ const IDLE_FORM_STATUS = Object.freeze({
13
+ pending: false,
14
+ data: null,
15
+ method: null,
16
+ action: null
17
+ });
18
+ const statuses = /* @__PURE__ */ new WeakMap();
19
+ const listeners = /* @__PURE__ */ new Set();
20
+ function setStatus(form, status) {
21
+ if (status) statuses.set(form, status);
22
+ else statuses.delete(form);
23
+ for (const listener of listeners) listener();
24
+ }
25
+ /** The submission in flight for a server form, or the idle status. */
26
+ function getServerFormStatus(form) {
27
+ return statuses.get(form) ?? IDLE_FORM_STATUS;
28
+ }
29
+ /** Calls `listener` whenever a server form starts or settles a submission. */
30
+ function subscribeServerForms(listener) {
31
+ listeners.add(listener);
32
+ return () => {
33
+ listeners.delete(listener);
34
+ };
35
+ }
36
+ /** Starts handling server-component forms on this page. Safe to call more than once. */
37
+ function enhanceForms() {
38
+ if (enhanced) return;
39
+ enhanced = true;
40
+ document.addEventListener("submit", (event) => {
41
+ if (event.defaultPrevented || !(event.target instanceof HTMLFormElement)) return;
42
+ const form = event.target;
43
+ const submitter = event.submitter ?? null;
44
+ const data = new FormData(form, submitter);
45
+ const posted = readPostedAction(data);
46
+ if (!posted) return;
47
+ event.preventDefault();
48
+ stripRuntimeFields(data);
49
+ form.setAttribute("aria-busy", "true");
50
+ const action = createServerReference(posted.id, posted.token);
51
+ setStatus(form, {
52
+ pending: true,
53
+ data,
54
+ method: form.method || "get",
55
+ action
56
+ });
57
+ action(data).then(() => form.reset()).catch((error) => console.error("[islx] The form's action failed:", error)).finally(() => {
58
+ form.removeAttribute("aria-busy");
59
+ setStatus(form, null);
60
+ });
61
+ });
62
+ }
63
+ export { subscribeServerForms as i, enhanceForms as n, getServerFormStatus as r, IDLE_FORM_STATUS as t };