@farm.js/islx 0.0.0-stage → 0.0.1

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 +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +198 -2
  4. package/SPEC.md +220 -0
  5. package/dist/_chunks/actions-BCtIGvrP.d.mts +58 -0
  6. package/dist/_chunks/actions-C4dZ1-XD.mjs +615 -0
  7. package/dist/_chunks/actions-CA_K-In7.mjs +310 -0
  8. package/dist/_chunks/actions-Eutk6UzT.mjs +326 -0
  9. package/dist/_chunks/build-COIcs7wm.mjs +9 -0
  10. package/dist/_chunks/codec-BTDkzzPP.mjs +271 -0
  11. package/dist/_chunks/codec-CK8kZp4u.d.mts +64 -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-BEV73f_8.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 +403 -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
@@ -0,0 +1,271 @@
1
+ import { N as getTag } from "./wire-CXOodE06.mjs";
2
+ // src/protocol/codec.ts
3
+ /**
4
+ * Values JSON cannot hold. Each travels as a tagged object: the reserved key `$` holds a short tag
5
+ * and `v` holds the data.
6
+ *
7
+ * undefined {"$":"u"}
8
+ * NaN, ±Infinity, -0 {"$":"n","v":"NaN"}
9
+ * 12345678901234567890n {"$":"B","v":"12345678901234567890"}
10
+ * new Date(…) {"$":"D","v":"2026-10-28T09:00:00.000Z"} (null for an invalid date)
11
+ * new Map([[k, v]]) {"$":"M","v":[[k, v]]} (keys and values tagged)
12
+ * new Set([v]) {"$":"S","v":[v]}
13
+ * new URL(…) {"$":"U","v":"https://example.com/"}
14
+ * /ab+c/gi {"$":"R","v":["ab+c","gi"]}
15
+ * Uint8Array, ArrayBuffer… {"$":"Y","v":["Uint8Array","base64…"]}
16
+ * app data with a "$" key {"$":"o","v":{…}} (never read as a tag)
17
+ *
18
+ * Decoding accepts input from the browser (server action arguments): it builds plain objects
19
+ * without assigning `__proto__`, and rejects unknown or malformed tags.
20
+ */
21
+ const TYPED_ARRAYS = {
22
+ Int8Array,
23
+ Uint8Array,
24
+ Uint8ClampedArray,
25
+ Int16Array,
26
+ Uint16Array,
27
+ Int32Array,
28
+ Uint32Array,
29
+ Float32Array,
30
+ Float64Array,
31
+ BigInt64Array,
32
+ BigUint64Array
33
+ };
34
+ /** Standard base64 of bytes. */
35
+ function toBase64(bytes) {
36
+ let binary = "";
37
+ for (let index = 0; index < bytes.length; index += 32768) binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
38
+ return btoa(binary);
39
+ }
40
+ /** Bytes of standard base64. Throws on anything else. */
41
+ function fromBase64(base64) {
42
+ const binary = atob(base64);
43
+ const bytes = new Uint8Array(binary.length);
44
+ for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
45
+ return bytes;
46
+ }
47
+ /** A tagged object. */
48
+ function tagged(tag, value) {
49
+ return value === void 0 ? { ["$"]: tag } : {
50
+ ["$"]: tag,
51
+ ["v"]: value
52
+ };
53
+ }
54
+ /** Whether a plain object must travel wrapped as `o`, because its own keys include `$`. */
55
+ function needsObjectTag(object) {
56
+ return Object.prototype.hasOwnProperty.call(object, "$");
57
+ }
58
+ /**
59
+ * The tag for a value JSON cannot hold and that has no children (everything above but `M`, `S`
60
+ * and `o`), or undefined when the value needs none.
61
+ */
62
+ function encodeLeaf(value) {
63
+ switch (typeof value) {
64
+ case "undefined": return tagged("u");
65
+ case "bigint": return tagged("B", value.toString());
66
+ case "number":
67
+ if (Number.isNaN(value)) return tagged("n", "NaN");
68
+ if (value === Infinity) return tagged("n", "Infinity");
69
+ if (value === -Infinity) return tagged("n", "-Infinity");
70
+ if (Object.is(value, -0)) return tagged("n", "-0");
71
+ return;
72
+ case "object": break;
73
+ default: return;
74
+ }
75
+ if (value === null) return void 0;
76
+ if (value instanceof Date) return tagged("D", Number.isNaN(value.getTime()) ? null : value.toISOString());
77
+ if (typeof URL === "function" && value instanceof URL) return tagged("U", value.href);
78
+ if (value instanceof RegExp) return tagged("R", [value.source, value.flags]);
79
+ if (value instanceof ArrayBuffer) return tagged("Y", ["ArrayBuffer", toBase64(new Uint8Array(value))]);
80
+ if (ArrayBuffer.isView(value)) {
81
+ const name = value instanceof DataView ? "DataView" : value.constructor.name;
82
+ if (name !== "DataView" && !(name in TYPED_ARRAYS)) return void 0;
83
+ return tagged("Y", [name, toBase64(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))]);
84
+ }
85
+ }
86
+ /** Returned by `decodeTagged` for the runtime's reference tags (`r`, `s`, `a`). */
87
+ const NOT_A_VALUE = Symbol.for("islx.codec.not-a-value");
88
+ /** A plain object from entries; `defineProperty`, so a "__proto__" key stays an ordinary key. */
89
+ function plainObject(entries) {
90
+ const object = {};
91
+ for (const [key, value] of entries) Object.defineProperty(object, key, {
92
+ value,
93
+ enumerable: true,
94
+ writable: true,
95
+ configurable: true
96
+ });
97
+ return object;
98
+ }
99
+ /**
100
+ * The value a tagged object stands for, decoding children (a `Map`'s or `Set`'s entries, an `o`
101
+ * object's properties) with `decodeChild`. Returns `NOT_A_VALUE` for the runtime's reference tags,
102
+ * and throws for an unknown or malformed tag.
103
+ */
104
+ function decodeTagged(object, decodeChild) {
105
+ const tag = object["$"];
106
+ const data = object["v"];
107
+ switch (tag) {
108
+ case "r":
109
+ case "s":
110
+ case "a":
111
+ case "F":
112
+ case "K": return NOT_A_VALUE;
113
+ case "o":
114
+ if (data && typeof data === "object" && !Array.isArray(data)) return plainObject(Object.entries(data).map(([key, value]) => [key, decodeChild(value)]));
115
+ break;
116
+ case "u": return;
117
+ case "n":
118
+ if (data === "NaN") return NaN;
119
+ if (data === "Infinity") return Infinity;
120
+ if (data === "-Infinity") return -Infinity;
121
+ if (data === "-0") return -0;
122
+ break;
123
+ case "B":
124
+ if (typeof data === "string" && /^-?\d+$/.test(data)) return BigInt(data);
125
+ break;
126
+ case "D":
127
+ if (data === null) return /* @__PURE__ */ new Date(NaN);
128
+ if (typeof data === "string") return new Date(data);
129
+ break;
130
+ case "U":
131
+ if (typeof data === "string") return new URL(data);
132
+ break;
133
+ case "R":
134
+ if (Array.isArray(data) && typeof data[0] === "string" && typeof data[1] === "string") return new RegExp(data[0], data[1]);
135
+ break;
136
+ case "Y": {
137
+ if (!Array.isArray(data) || typeof data[0] !== "string" || typeof data[1] !== "string") break;
138
+ const bytes = fromBase64(data[1]);
139
+ if (data[0] === "ArrayBuffer") return bytes.buffer;
140
+ if (data[0] === "DataView") return new DataView(bytes.buffer);
141
+ const TypedArray = Object.prototype.hasOwnProperty.call(TYPED_ARRAYS, data[0]) ? TYPED_ARRAYS[data[0]] : void 0;
142
+ if (!TypedArray || bytes.byteLength % TypedArray.BYTES_PER_ELEMENT !== 0) break;
143
+ return new TypedArray(bytes.buffer, 0, bytes.byteLength / TypedArray.BYTES_PER_ELEMENT);
144
+ }
145
+ case "M":
146
+ if (Array.isArray(data) && data.every((entry) => Array.isArray(entry) && entry.length === 2)) return new Map(data.map(([key, value]) => [decodeChild(key), decodeChild(value)]));
147
+ break;
148
+ case "S":
149
+ if (Array.isArray(data)) return new Set(data.map(decodeChild));
150
+ break;
151
+ default: throw new TypeError(`unknown ${JSON.stringify(tag)} tag`);
152
+ }
153
+ throw new TypeError(`malformed ${JSON.stringify(tag)} tag`);
154
+ }
155
+ /** Describes where in a value encoding failed, for error messages: `user.roles[2]`. */
156
+ function describePath(path) {
157
+ return path.map((part, index) => typeof part === "number" ? `[${part}]` : /^[A-Za-z_$][\w$]*$/.test(part) ? `${index === 0 ? "" : "."}${part}` : `[${JSON.stringify(part)}]`).join("");
158
+ }
159
+ /** Thrown by `encode` for a value it cannot carry; names where the value sits. */
160
+ var EncodeError = class extends TypeError {
161
+ /** Where the value sits, from the root: `["user", "roles", 2]`. */
162
+ path;
163
+ /** What is wrong with it: "functions are not serializable". */
164
+ reason;
165
+ constructor(reason, path) {
166
+ super(path.length > 0 ? `${describePath(path)}: ${reason}` : reason);
167
+ this.name = "EncodeError";
168
+ this.path = [...path];
169
+ this.reason = reason;
170
+ }
171
+ /** The path as written in code: `user.roles[2]`. */
172
+ get location() {
173
+ return describePath(this.path);
174
+ }
175
+ };
176
+ /**
177
+ * Encodes a value for JSON with the tags above: plain objects, arrays and the types listed.
178
+ * Symbols, class instances, circular values and (without `encodeFunction`) functions throw an
179
+ * `EncodeError` that names where the value sits.
180
+ */
181
+ function encode(value, options = {}) {
182
+ const seen = /* @__PURE__ */ new Set();
183
+ const path = [...options.path ?? []];
184
+ const fail = (reason) => {
185
+ throw new EncodeError(reason, path);
186
+ };
187
+ const visit = (value) => {
188
+ const leaf = encodeLeaf(value);
189
+ if (leaf) return leaf;
190
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
191
+ if (typeof value === "number") return value;
192
+ if (typeof value === "function") {
193
+ const encoded = options.encodeFunction?.(value);
194
+ return encoded === void 0 ? fail("functions are not serializable") : encoded;
195
+ }
196
+ if (typeof value !== "object") return fail(`${typeof value} values are not serializable`);
197
+ if (seen.has(value)) return fail("circular values are not serializable");
198
+ seen.add(value);
199
+ try {
200
+ if (Array.isArray(value)) return value.map((entry, index) => {
201
+ path.push(index);
202
+ try {
203
+ return visit(entry);
204
+ } finally {
205
+ path.pop();
206
+ }
207
+ });
208
+ if (value instanceof Map) {
209
+ let index = 0;
210
+ return tagged("M", Array.from(value, ([key, entry]) => {
211
+ path.push(index++);
212
+ try {
213
+ return [visit(key), visit(entry)];
214
+ } finally {
215
+ path.pop();
216
+ }
217
+ }));
218
+ }
219
+ if (value instanceof Set) {
220
+ let index = 0;
221
+ return tagged("S", Array.from(value, (entry) => {
222
+ path.push(index++);
223
+ try {
224
+ return visit(entry);
225
+ } finally {
226
+ path.pop();
227
+ }
228
+ }));
229
+ }
230
+ if (typeof value.$$typeof === "symbol") return fail("elements (JSX) are not serializable");
231
+ const prototype = Object.getPrototypeOf(value);
232
+ if (prototype !== Object.prototype && prototype !== null) {
233
+ const special = options.encodeObject?.(value);
234
+ if (special !== void 0) return special;
235
+ const name = value.constructor?.name;
236
+ return fail(`class instances${name ? ` (${name})` : ""} are not serializable`);
237
+ }
238
+ const encoded = plainObject(Object.entries(value).map(([key, entry]) => {
239
+ path.push(key);
240
+ try {
241
+ return [key, visit(entry)];
242
+ } finally {
243
+ path.pop();
244
+ }
245
+ }));
246
+ return needsObjectTag(value) ? tagged("o", encoded) : encoded;
247
+ } finally {
248
+ seen.delete(value);
249
+ }
250
+ };
251
+ return visit(value);
252
+ }
253
+ /** Decodes what `encode` produced, after `JSON.parse`. Reference tags are rejected by default. */
254
+ function decode(value, options = {}) {
255
+ const visit = (value) => {
256
+ if (Array.isArray(value)) return value.map(visit);
257
+ if (!value || typeof value !== "object") return value;
258
+ if (getTag(value) !== void 0) {
259
+ const object = value;
260
+ const decoded = decodeTagged(object, visit);
261
+ if (decoded !== NOT_A_VALUE) return decoded;
262
+ const reference = options.decodeReference?.(object);
263
+ if (reference !== void 0) return reference;
264
+ throw new TypeError(`a ${JSON.stringify(object["$"])} reference is not allowed here`);
265
+ }
266
+ if (needsObjectTag(value)) throw new TypeError("malformed \"$\" tag");
267
+ return plainObject(Object.entries(value).map(([key, entry]) => [key, visit(entry)]));
268
+ };
269
+ return visit(value);
270
+ }
271
+ export { encode as a, needsObjectTag as c, toBase64 as d, decodeTagged as i, plainObject as l, NOT_A_VALUE as n, encodeLeaf as o, decode as r, fromBase64 as s, EncodeError as t, tagged as u };
@@ -0,0 +1,64 @@
1
+ /** Standard base64 of bytes. */
2
+ declare function toBase64(bytes: Uint8Array): string;
3
+ /** Bytes of standard base64. Throws on anything else. */
4
+ declare function fromBase64(base64: string): Uint8Array;
5
+ /** A tagged object. */
6
+ declare function tagged(tag: string, value?: unknown): Record<string, unknown>;
7
+ /** Whether a plain object must travel wrapped as `o`, because its own keys include `$`. */
8
+ declare function needsObjectTag(object: object): boolean;
9
+ /**
10
+ * The tag for a value JSON cannot hold and that has no children (everything above but `M`, `S`
11
+ * and `o`), or undefined when the value needs none.
12
+ */
13
+ declare function encodeLeaf(value: unknown): Record<string, unknown> | undefined;
14
+ /** Returned by `decodeTagged` for the runtime's reference tags (`r`, `s`, `a`). */
15
+ declare const NOT_A_VALUE: unique symbol;
16
+ /** A plain object from entries; `defineProperty`, so a "__proto__" key stays an ordinary key. */
17
+ declare function plainObject(entries: Iterable<[string, unknown]>): Record<string, unknown>;
18
+ /**
19
+ * The value a tagged object stands for, decoding children (a `Map`'s or `Set`'s entries, an `o`
20
+ * object's properties) with `decodeChild`. Returns `NOT_A_VALUE` for the runtime's reference tags,
21
+ * and throws for an unknown or malformed tag.
22
+ */
23
+ declare function decodeTagged(object: Record<string, unknown>, decodeChild: (value: unknown) => unknown): unknown;
24
+ /** Thrown by `encode` for a value it cannot carry; names where the value sits. */
25
+ declare class EncodeError extends TypeError {
26
+ /** Where the value sits, from the root: `["user", "roles", 2]`. */
27
+ readonly path: readonly (string | number)[];
28
+ /** What is wrong with it: "functions are not serializable". */
29
+ readonly reason: string;
30
+ constructor(reason: string, path: readonly (string | number)[]);
31
+ /** The path as written in code: `user.roles[2]`. */
32
+ get location(): string;
33
+ }
34
+ interface EncodeOptions {
35
+ /**
36
+ * Encodes a function, such as a server action, or returns undefined to refuse it. Without it,
37
+ * functions throw.
38
+ */
39
+ encodeFunction?: (value: Function) => unknown;
40
+ /**
41
+ * Encodes an object the codec cannot carry itself (`FormData`, a `Blob`), or returns
42
+ * undefined to refuse it.
43
+ */
44
+ encodeObject?: (value: object) => unknown;
45
+ /** Names the root of the value in error messages. */
46
+ path?: readonly (string | number)[];
47
+ }
48
+ /**
49
+ * Encodes a value for JSON with the tags above: plain objects, arrays and the types listed.
50
+ * Symbols, class instances, circular values and (without `encodeFunction`) functions throw an
51
+ * `EncodeError` that names where the value sits.
52
+ */
53
+ declare function encode(value: unknown, options?: EncodeOptions): unknown;
54
+ interface DecodeOptions {
55
+ /**
56
+ * Decodes one of the runtime's reference tags (`r`, `s`, `a`), or returns undefined to refuse
57
+ * it. Without it, every reference throws: input from a browser must not name the runtime's own
58
+ * references unless the caller checks them.
59
+ */
60
+ decodeReference?: (object: Record<string, unknown>) => unknown;
61
+ }
62
+ /** Decodes what `encode` produced, after `JSON.parse`. Reference tags are rejected by default. */
63
+ declare function decode(value: unknown, options?: DecodeOptions): unknown;
64
+ export { decode as a, encodeLeaf as c, plainObject as d, tagged as f, NOT_A_VALUE as i, fromBase64 as l, EncodeError as n, decodeTagged as o, toBase64 as p, EncodeOptions as r, encode as s, DecodeOptions as t, needsObjectTag as u };
@@ -0,0 +1,35 @@
1
+ /** React's form state: the result, the hook it belongs to, the action id, its bound count. */
2
+ type ReactFormState = [state: unknown, key: string, referenceId: string, boundCount: number];
3
+ interface ActionErrorOutcome {
4
+ /** Send the browser here: a 303 for forms, `{"redirect"}` for calls. */
5
+ redirect?: string;
6
+ /** Answer with this status (and no details). */
7
+ status?: number;
8
+ }
9
+ interface HandleActionOptions {
10
+ /** Origins or hosts, besides the request's own, that may post actions: `*.example.com`. */
11
+ allowedOrigins?: readonly string[];
12
+ /** The largest body accepted, in bytes. Default 1 MB. */
13
+ bodySizeLimit?: number;
14
+ /**
15
+ * Runs the action, so the framework can give it the request's context (cookies, headers,
16
+ * an abort signal).
17
+ */
18
+ run?<T>(request: Request, action: () => Promise<T>): Promise<T>;
19
+ /**
20
+ * Maps an error the action threw to a redirect or a status: the framework's `redirect()` and
21
+ * `notFound()`. Anything unmapped is a 500, logged with `onError`.
22
+ */
23
+ mapError?(error: unknown): ActionErrorOutcome | undefined;
24
+ /**
25
+ * Renders the page after a form posted from a `useActionState` hook, with the action's result
26
+ * as React's `formState` (`renderToReadableStream(…, { formState })`, and the same to
27
+ * `hydrateRoot`). Without it, the form redirects back and the result is not shown.
28
+ */
29
+ renderFormState?(formState: ReactFormState, request: Request): Response | Promise<Response>;
30
+ /** Reports an action that failed. Default `console.error`. */
31
+ onError?(error: unknown): void;
32
+ }
33
+ /** Runs the server action a request posts, or returns null when it posts none. */
34
+ declare function handleAction(request: Request, options?: HandleActionOptions): Promise<Response | null>;
35
+ export { handleAction as i, HandleActionOptions as n, ReactFormState as r, ActionErrorOutcome as t };
@@ -0,0 +1,30 @@
1
+ import { t as ActionErrorOutcome } from "./handle-C0N9ZyNV.mjs";
2
+
3
+ // src/server/hooks.d.ts
4
+
5
+ interface ActionInfo {
6
+ request: Request;
7
+ /** The action's id. */
8
+ id: string;
9
+ /** Its export name, or the inline function's name. */
10
+ name: string;
11
+ /** Its module, from the app root. */
12
+ module: string;
13
+ }
14
+ interface ServerHooks {
15
+ /**
16
+ * Wraps every action: check the caller, rate-limit, log, time. Call `run` to run the action,
17
+ * and return what it returns; throw to refuse (`actionError` maps the error to a response).
18
+ */
19
+ action?(info: ActionInfo, run: () => Promise<unknown>): Promise<unknown>;
20
+ /** Maps an error an action threw to a redirect or a status: a framework's `redirect()`. */
21
+ actionError?(error: unknown, info: ActionInfo): ActionErrorOutcome | undefined;
22
+ /** Reports an action that failed with an error nothing mapped. */
23
+ error?(error: unknown, info: ActionInfo): void;
24
+ }
25
+ /** Types an app's server hooks. */
26
+ declare function defineServerHooks(hooks: ServerHooks): ServerHooks;
27
+ /** Installs the app's hooks; the plugin's server runtime calls it. */
28
+ declare function setServerHooks(hooks: ServerHooks | undefined): void;
29
+ declare function getServerHooks(): ServerHooks;
30
+ export { setServerHooks as a, getServerHooks as i, ServerHooks as n, defineServerHooks as r, ActionInfo as t };
@@ -0,0 +1,64 @@
1
+ import { n as IslandStrategy } from "./schedule-Dw14dW0H.mjs";
2
+
3
+ // src/client/hooks.d.ts
4
+
5
+ interface ClientHookEvents {
6
+ /** Before the next page is fetched. `preventDefault()` stays on this page. */
7
+ navigate: {
8
+ url: URL;
9
+ from: URL;
10
+ kind: "link" | "history" | "code";
11
+ };
12
+ /** The next page is in place. */
13
+ navigated: {
14
+ url: URL;
15
+ duration: number;
16
+ };
17
+ /** A navigation fell back to loading the page the ordinary way. */
18
+ navigateError: {
19
+ url: URL;
20
+ reason: string;
21
+ };
22
+ /** An island became interactive: `duration` runs from scheduling to React's first commit. */
23
+ hydrate: {
24
+ element: Element;
25
+ src: string;
26
+ export: string;
27
+ strategy: IslandStrategy;
28
+ duration: number;
29
+ };
30
+ /** An island failed; its server HTML was restored. */
31
+ islandError: {
32
+ element: Element;
33
+ src: string;
34
+ export: string;
35
+ error: unknown;
36
+ };
37
+ /** A server action call starts. `name` is the export (or inline function) name when known. */
38
+ action: {
39
+ id: string;
40
+ name?: string;
41
+ };
42
+ /** A server action call finished. */
43
+ actionEnd: {
44
+ id: string;
45
+ name?: string;
46
+ ok: boolean;
47
+ duration: number;
48
+ redirect?: string;
49
+ error?: unknown;
50
+ };
51
+ }
52
+ type ClientHookEvent<Name extends keyof ClientHookEvents> = ClientHookEvents[Name] & {
53
+ /** Only `navigate` can be cancelled. */
54
+ preventDefault(): void;
55
+ readonly defaultPrevented: boolean;
56
+ };
57
+ type ClientHooks = { [Name in keyof ClientHookEvents]?: (event: ClientHookEvent<Name>) => void };
58
+ /** Types an app's hooks module. */
59
+ declare function defineClientHooks(hooks: ClientHooks): ClientHooks;
60
+ /** Installs the app's hooks; the plugin's browser entry calls it. */
61
+ declare function setClientHooks(hooks: ClientHooks | undefined): void;
62
+ /** The DOM event name of a hook: `navigateError` → `f:navigate-error`. */
63
+ declare function hookEventName(name: keyof ClientHookEvents): string;
64
+ export { hookEventName as a, defineClientHooks as i, ClientHookEvents as n, setClientHooks as o, ClientHooks as r, ClientHookEvent as t };
@@ -0,0 +1,36 @@
1
+ // src/client/prehydration.ts
2
+ /**
3
+ * Holding interactions that happen before the islx script has run. An inline script in the page
4
+ * captures a click or a submit on an island that has not hydrated (it is prevented and queued),
5
+ * and announces a pointerdown or a focus (which go through). The runtime then hydrates those
6
+ * islands first and replays what was held.
7
+ *
8
+ * The script is inlined into every page, so it stays hand-minified and imports nothing.
9
+ */
10
+ /** Where the inline script queues held interactions: `{ target, kind }`. */
11
+ const PENDING_INTERACTIONS = "__islxPending";
12
+ /** The DOM event the script dispatches for each interaction it sees. */
13
+ const INTERACTION_EVENT = "f:island-interaction";
14
+ /** Elements whose click is held and replayed rather than merely observed. */
15
+ const REPLAYABLE_SELECTOR = "button,[role=\"button\"],input[type=\"button\"],input[type=\"submit\"],input[type=\"reset\"]";
16
+ /** The inline script's source (no `<script>` tag): pass it to the page's head or bootstrap. */
17
+ const PREHYDRATION_SCRIPT = `(function(){if(window.${PENDING_INTERACTIONS})return;var q=window.${PENDING_INTERACTIONS}=[];function island(n){var i=n&&n.closest&&n.closest("f-island");return i&&!i.hasAttribute("hydrated")?i:null}function tell(t,k){document.dispatchEvent(new CustomEvent("${INTERACTION_EVENT}",{detail:{target:t,kind:k}}))}function hold(e,t,k){if(!q.some(function(p){return p.target===t})){q.push({target:t,kind:k});tell(t,k)}e.preventDefault();e.stopImmediatePropagation()}document.addEventListener("click",function(e){if(e.defaultPrevented||e.button!==0||e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)return;var t=e.target&&e.target.closest&&e.target.closest(${JSON.stringify(REPLAYABLE_SELECTOR)});if(!t||t.closest("a[href]")||!island(t))return;hold(e,t,"click")},true);document.addEventListener("submit",function(e){var f=e.target;if(e.defaultPrevented||!f||f.nodeName!=="FORM"||!island(f))return;hold(e,f,"submit")},true);function seen(e){if(island(e.target))tell(e.target,null)}document.addEventListener("pointerdown",seen,true);document.addEventListener("focusin",seen,true)})();`;
18
+ /** Takes the interactions the inline script held inside `container`, oldest first. */
19
+ function takePendingInteractions(container) {
20
+ const queue = globalThis[PENDING_INTERACTIONS];
21
+ if (!Array.isArray(queue)) return [];
22
+ const taken = [];
23
+ for (let index = queue.length - 1; index >= 0; index--) {
24
+ const item = queue[index];
25
+ if (!(item?.target instanceof Element) || !container.contains(item.target)) continue;
26
+ queue.splice(index, 1);
27
+ taken.unshift(item);
28
+ }
29
+ return taken;
30
+ }
31
+ /** Whether the inline script holds an interaction inside `container`. */
32
+ function hasPendingInteraction(container) {
33
+ const queue = globalThis[PENDING_INTERACTIONS];
34
+ return Array.isArray(queue) && queue.some((item) => item?.target instanceof Element && container.contains(item.target));
35
+ }
36
+ export { hasPendingInteraction as a, REPLAYABLE_SELECTOR as i, PENDING_INTERACTIONS as n, takePendingInteractions as o, PREHYDRATION_SCRIPT as r, INTERACTION_EVENT as t };