@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.
- package/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +198 -2
- package/SPEC.md +220 -0
- package/dist/_chunks/actions-2sTj0A2T.mjs +306 -0
- package/dist/_chunks/actions-B8dJ8f7L.mjs +615 -0
- package/dist/_chunks/actions-BCtIGvrP.d.mts +58 -0
- package/dist/_chunks/actions-Eutk6UzT.mjs +326 -0
- package/dist/_chunks/build-COIcs7wm.mjs +9 -0
- package/dist/_chunks/codec-CK8kZp4u.d.mts +64 -0
- package/dist/_chunks/codec-DqfVYKsx.mjs +279 -0
- package/dist/_chunks/handle-C0N9ZyNV.d.mts +35 -0
- package/dist/_chunks/hooks-3VZV8f_d.d.mts +30 -0
- package/dist/_chunks/hooks-De0gNpOn.d.mts +64 -0
- package/dist/_chunks/prehydration-B1GLTLdk.mjs +36 -0
- package/dist/_chunks/schedule-BFoGKRrU.mjs +216 -0
- package/dist/_chunks/schedule-Dw14dW0H.d.mts +17 -0
- package/dist/_chunks/tree-BDB5VEao.mjs +777 -0
- package/dist/_chunks/tree-fID6sz25.d.mts +102 -0
- package/dist/_chunks/value-store-Dm-7xJ6W.d.mts +19 -0
- package/dist/_chunks/wire-C3Rpbwl6.d.mts +98 -0
- package/dist/_chunks/wire-CXOodE06.mjs +121 -0
- package/dist/client.d.mts +58 -0
- package/dist/client.mjs +197 -0
- package/dist/compiler.d.mts +2 -0
- package/dist/compiler.mjs +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/react/client.d.mts +49 -0
- package/dist/react/client.mjs +456 -0
- package/dist/react/server.d.mts +51 -0
- package/dist/react/server.mjs +84 -0
- package/dist/react.d.mts +3 -0
- package/dist/react.mjs +1 -0
- package/dist/server.d.mts +200 -0
- package/dist/server.mjs +378 -0
- package/dist/vite.d.mts +57 -0
- package/dist/vite.mjs +497 -0
- package/package.json +121 -4
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { A as VALUE_SCRIPT_TYPE, N as getTag, O as VALUES_HEADER, j as attachmentField, k as VALUE_KEY_PATTERN, o as BOUND_FIELD, s as CALL_FIELD, t as ACTION_HEADER } from "./wire-CXOodE06.mjs";
|
|
2
|
+
import { a as encode, r as decode, u as setOwn } from "./codec-DqfVYKsx.mjs";
|
|
3
|
+
// src/client/value-store.ts
|
|
4
|
+
/**
|
|
5
|
+
* The browser's store of values sent once. It keeps every value it has read, parses each once,
|
|
6
|
+
* and gives islands that share a value the same object. A navigation names the keys it holds
|
|
7
|
+
* (`valueHeaders()`), so the server leaves those values out.
|
|
8
|
+
*/
|
|
9
|
+
/** Past this many values the store starts over at the next navigation. */
|
|
10
|
+
const MAX_STORED_VALUES = 1024;
|
|
11
|
+
const STORE_KEY = Symbol.for("islx.values");
|
|
12
|
+
function createValueStore() {
|
|
13
|
+
const entries = /* @__PURE__ */ new Map();
|
|
14
|
+
const read = /* @__PURE__ */ new WeakSet();
|
|
15
|
+
const get = (key) => {
|
|
16
|
+
const entry = entries.get(key);
|
|
17
|
+
if (!entry) throw new Error(`islx value ${key} was not found`);
|
|
18
|
+
entries.delete(key);
|
|
19
|
+
entries.set(key, entry);
|
|
20
|
+
if (entry.state === "resolved") return entry.value;
|
|
21
|
+
if (entry.state === "resolving") throw new Error(`islx value ${key} refers to itself`);
|
|
22
|
+
entry.state = "resolving";
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(entry.json);
|
|
25
|
+
entry.value = entry.json.includes(`"\$"`) ? resolve(parsed) : parsed;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
entry.state = "raw";
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
entry.state = "resolved";
|
|
31
|
+
entry.json = "";
|
|
32
|
+
return entry.value;
|
|
33
|
+
};
|
|
34
|
+
const resolve = (value) => {
|
|
35
|
+
if (Array.isArray(value)) return value.map(resolve);
|
|
36
|
+
if (!value || typeof value !== "object") return value;
|
|
37
|
+
const object = value;
|
|
38
|
+
const tag = getTag(object);
|
|
39
|
+
if (tag === "r" && typeof object["v"] === "string") return get(object["v"]);
|
|
40
|
+
if (tag === "o" && object["v"] && typeof object["v"] === "object") return {
|
|
41
|
+
["$"]: "o",
|
|
42
|
+
["v"]: resolveEntries(object["v"])
|
|
43
|
+
};
|
|
44
|
+
return resolveEntries(object);
|
|
45
|
+
};
|
|
46
|
+
const resolveEntries = (object) => {
|
|
47
|
+
const resolved = {};
|
|
48
|
+
for (const key of Object.keys(object)) setOwn(resolved, key, resolve(object[key]));
|
|
49
|
+
return resolved;
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
ingest(root) {
|
|
53
|
+
for (const script of root.querySelectorAll(`script[type="${VALUE_SCRIPT_TYPE}"]`)) {
|
|
54
|
+
if (read.has(script)) continue;
|
|
55
|
+
read.add(script);
|
|
56
|
+
const key = script.getAttribute("name") ?? "";
|
|
57
|
+
if (VALUE_KEY_PATTERN.test(key) && !entries.has(key)) entries.set(key, {
|
|
58
|
+
json: script.textContent || "null",
|
|
59
|
+
state: "raw"
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
resolve,
|
|
64
|
+
heldHeader() {
|
|
65
|
+
if (entries.size > MAX_STORED_VALUES) entries.clear();
|
|
66
|
+
if (entries.size === 0) return void 0;
|
|
67
|
+
return [...entries.keys()].slice(-128).join(",");
|
|
68
|
+
},
|
|
69
|
+
clear() {
|
|
70
|
+
entries.clear();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** The page's value store, shared by every copy of islx on the page. */
|
|
75
|
+
function getValueStore() {
|
|
76
|
+
const scope = globalThis;
|
|
77
|
+
return scope[STORE_KEY] ??= createValueStore();
|
|
78
|
+
}
|
|
79
|
+
/** Headers a navigation sends so the server can leave out values the browser holds. */
|
|
80
|
+
function valueHeaders() {
|
|
81
|
+
const held = globalThis[STORE_KEY]?.heldHeader();
|
|
82
|
+
return held ? { [VALUES_HEADER]: held } : {};
|
|
83
|
+
}
|
|
84
|
+
// src/client/hooks.ts
|
|
85
|
+
const HOOKS_KEY = Symbol.for("islx.client.hooks");
|
|
86
|
+
const CANCELABLE = /* @__PURE__ */ new Set(["navigate"]);
|
|
87
|
+
/** Types an app's hooks module. */
|
|
88
|
+
function defineClientHooks(hooks) {
|
|
89
|
+
return hooks;
|
|
90
|
+
}
|
|
91
|
+
/** Installs the app's hooks; the plugin's browser entry calls it. */
|
|
92
|
+
function setClientHooks(hooks) {
|
|
93
|
+
globalThis[HOOKS_KEY] = hooks;
|
|
94
|
+
}
|
|
95
|
+
/** The DOM event name of a hook: `navigateError` → `f:navigate-error`. */
|
|
96
|
+
function hookEventName(name) {
|
|
97
|
+
return `f:${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Runs a hook and dispatches its DOM event. Returns false when it was cancelled. A hook that
|
|
101
|
+
* throws is reported, never allowed to break navigation or hydration.
|
|
102
|
+
*/
|
|
103
|
+
function emit(name, detail) {
|
|
104
|
+
const cancelable = CANCELABLE.has(name);
|
|
105
|
+
let prevented = false;
|
|
106
|
+
const event = {
|
|
107
|
+
...detail,
|
|
108
|
+
preventDefault() {
|
|
109
|
+
if (cancelable) prevented = true;
|
|
110
|
+
},
|
|
111
|
+
get defaultPrevented() {
|
|
112
|
+
return prevented;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const hook = globalThis[HOOKS_KEY]?.[name];
|
|
116
|
+
if (hook) try {
|
|
117
|
+
hook(event);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`[islx] The ${name} hook failed:`, error);
|
|
120
|
+
}
|
|
121
|
+
if (typeof document !== "undefined" && typeof CustomEvent === "function") {
|
|
122
|
+
const dom = new CustomEvent(hookEventName(name), {
|
|
123
|
+
detail,
|
|
124
|
+
cancelable
|
|
125
|
+
});
|
|
126
|
+
if (!document.dispatchEvent(dom)) prevented = cancelable;
|
|
127
|
+
}
|
|
128
|
+
return !prevented;
|
|
129
|
+
}
|
|
130
|
+
// src/client/actions.ts
|
|
131
|
+
/**
|
|
132
|
+
* The browser side of server actions. A client module's import of a `"use server"` module
|
|
133
|
+
* becomes references made here, and an action passed as a prop arrives as one.
|
|
134
|
+
*
|
|
135
|
+
* Calls run one at a time, in order, as React expects of actions. After each, the page refreshes
|
|
136
|
+
* (through the framework's router) so it shows what the action changed; a redirect from the
|
|
137
|
+
* action navigates instead. An action this server no longer knows, as after a deploy, reloads the
|
|
138
|
+
* page.
|
|
139
|
+
*/
|
|
140
|
+
const STATE_KEY = Symbol.for("islx.actions.client");
|
|
141
|
+
function state() {
|
|
142
|
+
const scope = globalThis;
|
|
143
|
+
return scope[STATE_KEY] ??= {
|
|
144
|
+
navigator: {},
|
|
145
|
+
queue: Promise.resolve(),
|
|
146
|
+
references: /* @__PURE__ */ new Map(),
|
|
147
|
+
info: /* @__PURE__ */ new WeakMap()
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Connects actions to the framework's router. */
|
|
151
|
+
function setActionNavigator(navigator) {
|
|
152
|
+
state().navigator = navigator;
|
|
153
|
+
}
|
|
154
|
+
/** Encodes an action passed back to the server as an argument. */
|
|
155
|
+
function encodeFunction(value) {
|
|
156
|
+
const reference = state().info.get(value);
|
|
157
|
+
if (!reference) return void 0;
|
|
158
|
+
if (reference.bound.length > 0) throw new TypeError("an action bound in the browser cannot be sent to the server");
|
|
159
|
+
return reference.token === void 0 ? {
|
|
160
|
+
["$"]: "a",
|
|
161
|
+
["v"]: reference.id
|
|
162
|
+
} : {
|
|
163
|
+
["$"]: "a",
|
|
164
|
+
["v"]: reference.id,
|
|
165
|
+
["b"]: reference.token
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Decodes an action in a result or props: `{"$":"a","v":id,"b"?:sealed}`. */
|
|
169
|
+
function decodeActionReference(object) {
|
|
170
|
+
if (object["$"] !== "a" || typeof object["v"] !== "string") return void 0;
|
|
171
|
+
const token = object["b"];
|
|
172
|
+
if (token !== void 0 && typeof token !== "string") return void 0;
|
|
173
|
+
return createServerReference(object["v"], token);
|
|
174
|
+
}
|
|
175
|
+
/** Returned by a call whose action this deploy no longer knows: the page reloads. */
|
|
176
|
+
const STALE = Symbol("islx.action.stale");
|
|
177
|
+
async function call(reference, args) {
|
|
178
|
+
const all = [...reference.bound, ...args];
|
|
179
|
+
const single = all.length === 1 && all[0] instanceof FormData ? all[0] : null;
|
|
180
|
+
let body;
|
|
181
|
+
const headers = { [ACTION_HEADER]: reference.id };
|
|
182
|
+
if (single) {
|
|
183
|
+
const form = new FormData();
|
|
184
|
+
for (const [key, value] of single) form.append(key, value);
|
|
185
|
+
if (reference.token !== void 0) form.append(BOUND_FIELD, reference.token);
|
|
186
|
+
body = form;
|
|
187
|
+
} else {
|
|
188
|
+
const attachments = [];
|
|
189
|
+
const encodeObject = (value) => {
|
|
190
|
+
if (value instanceof FormData || value instanceof Blob) {
|
|
191
|
+
attachments.push(value);
|
|
192
|
+
return {
|
|
193
|
+
["$"]: value instanceof FormData ? "F" : "K",
|
|
194
|
+
["v"]: attachments.length - 1
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const json = JSON.stringify({
|
|
199
|
+
a: encode(all, {
|
|
200
|
+
encodeFunction,
|
|
201
|
+
encodeObject
|
|
202
|
+
}),
|
|
203
|
+
b: reference.token
|
|
204
|
+
});
|
|
205
|
+
if (attachments.length === 0) {
|
|
206
|
+
headers["content-type"] = "text/plain;charset=UTF-8";
|
|
207
|
+
body = json;
|
|
208
|
+
} else {
|
|
209
|
+
const form = new FormData();
|
|
210
|
+
form.append(CALL_FIELD, json);
|
|
211
|
+
attachments.forEach((attachment, index) => {
|
|
212
|
+
if (attachment instanceof FormData) for (const [key, value] of attachment) form.append(attachmentField(index, key), value);
|
|
213
|
+
else form.append(attachmentField(index), attachment);
|
|
214
|
+
});
|
|
215
|
+
body = form;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const response = await fetch(location.href, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers,
|
|
221
|
+
body,
|
|
222
|
+
credentials: "same-origin"
|
|
223
|
+
});
|
|
224
|
+
if (response.headers.get("F-Action-Stale")) {
|
|
225
|
+
location.reload();
|
|
226
|
+
return STALE;
|
|
227
|
+
}
|
|
228
|
+
if (!response.ok) throw new Error(`Server action failed: ${response.status}`);
|
|
229
|
+
const payload = await response.json();
|
|
230
|
+
const navigator = state().navigator;
|
|
231
|
+
if (typeof payload.redirect === "string") {
|
|
232
|
+
reference.redirect = payload.redirect;
|
|
233
|
+
if (navigator.navigate) await navigator.navigate(payload.redirect);
|
|
234
|
+
else location.assign(payload.redirect);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const result = decode(payload.result, { decodeReference: decodeActionReference });
|
|
238
|
+
if (navigator.refresh) await navigator.refresh();
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
function createReference(reference) {
|
|
242
|
+
const client = state();
|
|
243
|
+
const fn = (...args) => {
|
|
244
|
+
const { id, name } = reference;
|
|
245
|
+
let startedAt = 0;
|
|
246
|
+
const run = async () => {
|
|
247
|
+
emit("action", {
|
|
248
|
+
id,
|
|
249
|
+
name
|
|
250
|
+
});
|
|
251
|
+
startedAt = performance.now();
|
|
252
|
+
reference.redirect = void 0;
|
|
253
|
+
return call(reference, args);
|
|
254
|
+
};
|
|
255
|
+
const result = client.queue.then(run).then((value) => {
|
|
256
|
+
if (value !== STALE) {
|
|
257
|
+
const { redirect } = reference;
|
|
258
|
+
emit("actionEnd", {
|
|
259
|
+
id,
|
|
260
|
+
name,
|
|
261
|
+
ok: true,
|
|
262
|
+
duration: performance.now() - startedAt,
|
|
263
|
+
redirect
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}, (error) => {
|
|
268
|
+
emit("actionEnd", {
|
|
269
|
+
id,
|
|
270
|
+
name,
|
|
271
|
+
ok: false,
|
|
272
|
+
duration: performance.now() - startedAt,
|
|
273
|
+
error
|
|
274
|
+
});
|
|
275
|
+
throw error;
|
|
276
|
+
});
|
|
277
|
+
client.queue = result.then(() => {}, () => {});
|
|
278
|
+
return result.then((value) => value === STALE ? new Promise(() => {}) : value);
|
|
279
|
+
};
|
|
280
|
+
client.info.set(fn, reference);
|
|
281
|
+
Object.defineProperty(fn, "bind", {
|
|
282
|
+
configurable: true,
|
|
283
|
+
value: (_thisArg, ...args) => createReference({
|
|
284
|
+
...reference,
|
|
285
|
+
bound: [...reference.bound, ...args]
|
|
286
|
+
})
|
|
287
|
+
});
|
|
288
|
+
return fn;
|
|
289
|
+
}
|
|
290
|
+
/** The function that calls a server action; one per id and sealed values, so props keep identity. */
|
|
291
|
+
function createServerReference(id, token, name) {
|
|
292
|
+
const key = token === void 0 ? id : `${id}\0${token}`;
|
|
293
|
+
const references = state().references;
|
|
294
|
+
let reference = references.get(key);
|
|
295
|
+
if (!reference) {
|
|
296
|
+
reference = createReference({
|
|
297
|
+
id,
|
|
298
|
+
token,
|
|
299
|
+
bound: [],
|
|
300
|
+
name
|
|
301
|
+
});
|
|
302
|
+
references.set(key, reference);
|
|
303
|
+
}
|
|
304
|
+
return reference;
|
|
305
|
+
}
|
|
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 };
|