@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.
- package/CHANGELOG.md +43 -0
- package/LICENSE +21 -0
- package/README.md +198 -2
- package/SPEC.md +220 -0
- package/dist/_chunks/actions-BCtIGvrP.d.mts +58 -0
- package/dist/_chunks/actions-C4dZ1-XD.mjs +615 -0
- package/dist/_chunks/actions-CA_K-In7.mjs +310 -0
- package/dist/_chunks/actions-Eutk6UzT.mjs +326 -0
- package/dist/_chunks/build-COIcs7wm.mjs +9 -0
- package/dist/_chunks/codec-BTDkzzPP.mjs +271 -0
- package/dist/_chunks/codec-CK8kZp4u.d.mts +64 -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-BEV73f_8.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 +403 -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,310 @@
|
|
|
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 } from "./codec-BTDkzzPP.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
|
+
entry.value = resolve(JSON.parse(entry.json));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
entry.state = "raw";
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
entry.state = "resolved";
|
|
30
|
+
entry.json = "";
|
|
31
|
+
return entry.value;
|
|
32
|
+
};
|
|
33
|
+
const resolve = (value) => {
|
|
34
|
+
if (Array.isArray(value)) return value.map(resolve);
|
|
35
|
+
if (!value || typeof value !== "object") return value;
|
|
36
|
+
const object = value;
|
|
37
|
+
const tag = getTag(object);
|
|
38
|
+
if (tag === "r" && typeof object["v"] === "string") return get(object["v"]);
|
|
39
|
+
if (tag === "o" && object["v"] && typeof object["v"] === "object") return {
|
|
40
|
+
["$"]: "o",
|
|
41
|
+
["v"]: resolveEntries(object["v"])
|
|
42
|
+
};
|
|
43
|
+
return resolveEntries(object);
|
|
44
|
+
};
|
|
45
|
+
const resolveEntries = (object) => {
|
|
46
|
+
const resolved = {};
|
|
47
|
+
for (const key of Object.keys(object)) Object.defineProperty(resolved, key, {
|
|
48
|
+
value: resolve(object[key]),
|
|
49
|
+
enumerable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
configurable: true
|
|
52
|
+
});
|
|
53
|
+
return resolved;
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
ingest(root) {
|
|
57
|
+
for (const script of root.querySelectorAll(`script[type="${VALUE_SCRIPT_TYPE}"]`)) {
|
|
58
|
+
if (read.has(script)) continue;
|
|
59
|
+
read.add(script);
|
|
60
|
+
const key = script.getAttribute("name") ?? "";
|
|
61
|
+
if (VALUE_KEY_PATTERN.test(key) && !entries.has(key)) entries.set(key, {
|
|
62
|
+
json: script.textContent || "null",
|
|
63
|
+
state: "raw"
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
resolve,
|
|
68
|
+
heldHeader() {
|
|
69
|
+
if (entries.size > MAX_STORED_VALUES) entries.clear();
|
|
70
|
+
if (entries.size === 0) return void 0;
|
|
71
|
+
return [...entries.keys()].slice(-128).join(",");
|
|
72
|
+
},
|
|
73
|
+
clear() {
|
|
74
|
+
entries.clear();
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** The page's value store, shared by every copy of islx on the page. */
|
|
79
|
+
function getValueStore() {
|
|
80
|
+
const scope = globalThis;
|
|
81
|
+
return scope[STORE_KEY] ??= createValueStore();
|
|
82
|
+
}
|
|
83
|
+
/** Headers a navigation sends so the server can leave out values the browser holds. */
|
|
84
|
+
function valueHeaders() {
|
|
85
|
+
const held = globalThis[STORE_KEY]?.heldHeader();
|
|
86
|
+
return held ? { [VALUES_HEADER]: held } : {};
|
|
87
|
+
}
|
|
88
|
+
// src/client/hooks.ts
|
|
89
|
+
const HOOKS_KEY = Symbol.for("islx.client.hooks");
|
|
90
|
+
const CANCELABLE = /* @__PURE__ */ new Set(["navigate"]);
|
|
91
|
+
/** Types an app's hooks module. */
|
|
92
|
+
function defineClientHooks(hooks) {
|
|
93
|
+
return hooks;
|
|
94
|
+
}
|
|
95
|
+
/** Installs the app's hooks; the plugin's browser entry calls it. */
|
|
96
|
+
function setClientHooks(hooks) {
|
|
97
|
+
globalThis[HOOKS_KEY] = hooks;
|
|
98
|
+
}
|
|
99
|
+
/** The DOM event name of a hook: `navigateError` → `f:navigate-error`. */
|
|
100
|
+
function hookEventName(name) {
|
|
101
|
+
return `f:${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Runs a hook and dispatches its DOM event. Returns false when it was cancelled. A hook that
|
|
105
|
+
* throws is reported, never allowed to break navigation or hydration.
|
|
106
|
+
*/
|
|
107
|
+
function emit(name, detail) {
|
|
108
|
+
const cancelable = CANCELABLE.has(name);
|
|
109
|
+
let prevented = false;
|
|
110
|
+
const event = {
|
|
111
|
+
...detail,
|
|
112
|
+
preventDefault() {
|
|
113
|
+
if (cancelable) prevented = true;
|
|
114
|
+
},
|
|
115
|
+
get defaultPrevented() {
|
|
116
|
+
return prevented;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const hook = globalThis[HOOKS_KEY]?.[name];
|
|
120
|
+
if (hook) try {
|
|
121
|
+
hook(event);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`[islx] The ${name} hook failed:`, error);
|
|
124
|
+
}
|
|
125
|
+
if (typeof document !== "undefined" && typeof CustomEvent === "function") {
|
|
126
|
+
const dom = new CustomEvent(hookEventName(name), {
|
|
127
|
+
detail,
|
|
128
|
+
cancelable
|
|
129
|
+
});
|
|
130
|
+
if (!document.dispatchEvent(dom)) prevented = cancelable;
|
|
131
|
+
}
|
|
132
|
+
return !prevented;
|
|
133
|
+
}
|
|
134
|
+
// src/client/actions.ts
|
|
135
|
+
/**
|
|
136
|
+
* The browser side of server actions. A client module's import of a `"use server"` module
|
|
137
|
+
* becomes references made here, and an action passed as a prop arrives as one.
|
|
138
|
+
*
|
|
139
|
+
* Calls run one at a time, in order, as React expects of actions. After each, the page refreshes
|
|
140
|
+
* (through the framework's router) so it shows what the action changed; a redirect from the
|
|
141
|
+
* action navigates instead. An action this server no longer knows, as after a deploy, reloads the
|
|
142
|
+
* page.
|
|
143
|
+
*/
|
|
144
|
+
const STATE_KEY = Symbol.for("islx.actions.client");
|
|
145
|
+
function state() {
|
|
146
|
+
const scope = globalThis;
|
|
147
|
+
return scope[STATE_KEY] ??= {
|
|
148
|
+
navigator: {},
|
|
149
|
+
queue: Promise.resolve(),
|
|
150
|
+
references: /* @__PURE__ */ new Map(),
|
|
151
|
+
info: /* @__PURE__ */ new WeakMap()
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Connects actions to the framework's router. */
|
|
155
|
+
function setActionNavigator(navigator) {
|
|
156
|
+
state().navigator = navigator;
|
|
157
|
+
}
|
|
158
|
+
/** Encodes an action passed back to the server as an argument. */
|
|
159
|
+
function encodeFunction(value) {
|
|
160
|
+
const reference = state().info.get(value);
|
|
161
|
+
if (!reference) return void 0;
|
|
162
|
+
if (reference.bound.length > 0) throw new TypeError("an action bound in the browser cannot be sent to the server");
|
|
163
|
+
return reference.token === void 0 ? {
|
|
164
|
+
["$"]: "a",
|
|
165
|
+
["v"]: reference.id
|
|
166
|
+
} : {
|
|
167
|
+
["$"]: "a",
|
|
168
|
+
["v"]: reference.id,
|
|
169
|
+
["b"]: reference.token
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** Decodes an action in a result or props: `{"$":"a","v":id,"b"?:sealed}`. */
|
|
173
|
+
function decodeActionReference(object) {
|
|
174
|
+
if (object["$"] !== "a" || typeof object["v"] !== "string") return void 0;
|
|
175
|
+
const token = object["b"];
|
|
176
|
+
if (token !== void 0 && typeof token !== "string") return void 0;
|
|
177
|
+
return createServerReference(object["v"], token);
|
|
178
|
+
}
|
|
179
|
+
/** Returned by a call whose action this deploy no longer knows: the page reloads. */
|
|
180
|
+
const STALE = Symbol("islx.action.stale");
|
|
181
|
+
async function call(reference, args) {
|
|
182
|
+
const all = [...reference.bound, ...args];
|
|
183
|
+
const single = all.length === 1 && all[0] instanceof FormData ? all[0] : null;
|
|
184
|
+
let body;
|
|
185
|
+
const headers = { [ACTION_HEADER]: reference.id };
|
|
186
|
+
if (single) {
|
|
187
|
+
const form = new FormData();
|
|
188
|
+
for (const [key, value] of single) form.append(key, value);
|
|
189
|
+
if (reference.token !== void 0) form.append(BOUND_FIELD, reference.token);
|
|
190
|
+
body = form;
|
|
191
|
+
} else {
|
|
192
|
+
const attachments = [];
|
|
193
|
+
const encodeObject = (value) => {
|
|
194
|
+
if (value instanceof FormData || value instanceof Blob) {
|
|
195
|
+
attachments.push(value);
|
|
196
|
+
return {
|
|
197
|
+
["$"]: value instanceof FormData ? "F" : "K",
|
|
198
|
+
["v"]: attachments.length - 1
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const json = JSON.stringify({
|
|
203
|
+
a: encode(all, {
|
|
204
|
+
encodeFunction,
|
|
205
|
+
encodeObject
|
|
206
|
+
}),
|
|
207
|
+
b: reference.token
|
|
208
|
+
});
|
|
209
|
+
if (attachments.length === 0) {
|
|
210
|
+
headers["content-type"] = "text/plain;charset=UTF-8";
|
|
211
|
+
body = json;
|
|
212
|
+
} else {
|
|
213
|
+
const form = new FormData();
|
|
214
|
+
form.append(CALL_FIELD, json);
|
|
215
|
+
attachments.forEach((attachment, index) => {
|
|
216
|
+
if (attachment instanceof FormData) for (const [key, value] of attachment) form.append(attachmentField(index, key), value);
|
|
217
|
+
else form.append(attachmentField(index), attachment);
|
|
218
|
+
});
|
|
219
|
+
body = form;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const response = await fetch(location.href, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers,
|
|
225
|
+
body,
|
|
226
|
+
credentials: "same-origin"
|
|
227
|
+
});
|
|
228
|
+
if (response.headers.get("F-Action-Stale")) {
|
|
229
|
+
location.reload();
|
|
230
|
+
return STALE;
|
|
231
|
+
}
|
|
232
|
+
if (!response.ok) throw new Error(`Server action failed: ${response.status}`);
|
|
233
|
+
const payload = await response.json();
|
|
234
|
+
const navigator = state().navigator;
|
|
235
|
+
if (typeof payload.redirect === "string") {
|
|
236
|
+
reference.redirect = payload.redirect;
|
|
237
|
+
if (navigator.navigate) await navigator.navigate(payload.redirect);
|
|
238
|
+
else location.assign(payload.redirect);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const result = decode(payload.result, { decodeReference: decodeActionReference });
|
|
242
|
+
if (navigator.refresh) await navigator.refresh();
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
function createReference(reference) {
|
|
246
|
+
const client = state();
|
|
247
|
+
const fn = (...args) => {
|
|
248
|
+
const { id, name } = reference;
|
|
249
|
+
let startedAt = 0;
|
|
250
|
+
const run = async () => {
|
|
251
|
+
emit("action", {
|
|
252
|
+
id,
|
|
253
|
+
name
|
|
254
|
+
});
|
|
255
|
+
startedAt = performance.now();
|
|
256
|
+
reference.redirect = void 0;
|
|
257
|
+
return call(reference, args);
|
|
258
|
+
};
|
|
259
|
+
const result = client.queue.then(run).then((value) => {
|
|
260
|
+
if (value !== STALE) {
|
|
261
|
+
const { redirect } = reference;
|
|
262
|
+
emit("actionEnd", {
|
|
263
|
+
id,
|
|
264
|
+
name,
|
|
265
|
+
ok: true,
|
|
266
|
+
duration: performance.now() - startedAt,
|
|
267
|
+
redirect
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return value;
|
|
271
|
+
}, (error) => {
|
|
272
|
+
emit("actionEnd", {
|
|
273
|
+
id,
|
|
274
|
+
name,
|
|
275
|
+
ok: false,
|
|
276
|
+
duration: performance.now() - startedAt,
|
|
277
|
+
error
|
|
278
|
+
});
|
|
279
|
+
throw error;
|
|
280
|
+
});
|
|
281
|
+
client.queue = result.then(() => {}, () => {});
|
|
282
|
+
return result.then((value) => value === STALE ? new Promise(() => {}) : value);
|
|
283
|
+
};
|
|
284
|
+
client.info.set(fn, reference);
|
|
285
|
+
Object.defineProperty(fn, "bind", {
|
|
286
|
+
configurable: true,
|
|
287
|
+
value: (_thisArg, ...args) => createReference({
|
|
288
|
+
...reference,
|
|
289
|
+
bound: [...reference.bound, ...args]
|
|
290
|
+
})
|
|
291
|
+
});
|
|
292
|
+
return fn;
|
|
293
|
+
}
|
|
294
|
+
/** The function that calls a server action; one per id and sealed values, so props keep identity. */
|
|
295
|
+
function createServerReference(id, token, name) {
|
|
296
|
+
const key = token === void 0 ? id : `${id}\0${token}`;
|
|
297
|
+
const references = state().references;
|
|
298
|
+
let reference = references.get(key);
|
|
299
|
+
if (!reference) {
|
|
300
|
+
reference = createReference({
|
|
301
|
+
id,
|
|
302
|
+
token,
|
|
303
|
+
bound: [],
|
|
304
|
+
name
|
|
305
|
+
});
|
|
306
|
+
references.set(key, reference);
|
|
307
|
+
}
|
|
308
|
+
return reference;
|
|
309
|
+
}
|
|
310
|
+
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 };
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { walk } from "estree-walker";
|
|
4
|
+
import isReference from "is-reference";
|
|
5
|
+
import MagicString from "magic-string";
|
|
6
|
+
import { analyze } from "periscopic";
|
|
7
|
+
// src/compiler/actions.ts
|
|
8
|
+
/**
|
|
9
|
+
* The server-action compiler: a pure function from a module's source to its source for the
|
|
10
|
+
* server or the browser. Bundler plugins call it; it parses with the parser they pass.
|
|
11
|
+
*
|
|
12
|
+
* - A `"use server"` module: on the server every export registers as an action; in the browser
|
|
13
|
+
* the module becomes references that call those ids, so its code never reaches a bundle.
|
|
14
|
+
* - An inline `"use server"` function in server code: hoisted to module scope, taking the
|
|
15
|
+
* variables it read from the component around it as one array. Where it was written, it
|
|
16
|
+
* becomes a reference that carries those variables, sealed.
|
|
17
|
+
*/
|
|
18
|
+
/** A mistake in the source, with where it is. Bundlers show `loc` with a code frame. */
|
|
19
|
+
var ActionCompileError = class extends Error {
|
|
20
|
+
loc;
|
|
21
|
+
id;
|
|
22
|
+
constructor(message, file, code, offset) {
|
|
23
|
+
const before = code.slice(0, offset).split("\n");
|
|
24
|
+
const line = before.length;
|
|
25
|
+
const column = before.at(-1).length;
|
|
26
|
+
super(`${message} (${file}:${line}:${column + 1})`);
|
|
27
|
+
this.name = "ActionCompileError";
|
|
28
|
+
this.id = file;
|
|
29
|
+
this.loc = {
|
|
30
|
+
file,
|
|
31
|
+
line,
|
|
32
|
+
column
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Whether the module's directive prologue (the strings before any other statement, after
|
|
38
|
+
* comments) holds `directive`. A linear scan, for a cheap check before parsing.
|
|
39
|
+
*/
|
|
40
|
+
function hasModuleDirective(code, directive) {
|
|
41
|
+
let index = 0;
|
|
42
|
+
while (index < code.length) {
|
|
43
|
+
const char = code.charCodeAt(index);
|
|
44
|
+
if (char === 32 || char >= 9 && char <= 13 || char === 65279 || char === 160) index++;
|
|
45
|
+
else if (code.startsWith("//", index)) {
|
|
46
|
+
const end = code.indexOf("\n", index);
|
|
47
|
+
if (end === -1) return false;
|
|
48
|
+
index = end + 1;
|
|
49
|
+
} else if (code.startsWith("/*", index)) {
|
|
50
|
+
const end = code.indexOf("*/", index + 2);
|
|
51
|
+
if (end === -1) return false;
|
|
52
|
+
index = end + 2;
|
|
53
|
+
} else if (char === 34 || char === 39) {
|
|
54
|
+
const quote = code[index];
|
|
55
|
+
const end = code.indexOf(quote, index + 1);
|
|
56
|
+
if (end === -1) return false;
|
|
57
|
+
const value = code.slice(index + 1, end);
|
|
58
|
+
index = end + 1;
|
|
59
|
+
while (index < code.length && /[ \t]/.test(code[index])) index++;
|
|
60
|
+
if (code[index] === ";") index++;
|
|
61
|
+
else if (index < code.length && code[index] !== "\n" && code[index] !== "\r") return false;
|
|
62
|
+
if (value === directive) return true;
|
|
63
|
+
} else return false;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/** The id an action has on both sides: a hash of its module path from the root and its name. */
|
|
68
|
+
function getActionId(root, file, name, salt = "") {
|
|
69
|
+
const relative = path.relative(root, file.split("?", 1)[0]).split(path.sep).join("/");
|
|
70
|
+
return createHash("sha256").update(`${salt}${relative}#${name}`).digest("base64url").slice(0, 22);
|
|
71
|
+
}
|
|
72
|
+
function hasDirective(statements, directive) {
|
|
73
|
+
for (const statement of statements ?? []) {
|
|
74
|
+
if (statement.type !== "ExpressionStatement") return false;
|
|
75
|
+
const expression = statement.expression;
|
|
76
|
+
const value = statement.directive ?? (expression?.type === "Literal" ? expression.value : void 0);
|
|
77
|
+
if (typeof value !== "string") return false;
|
|
78
|
+
if (value === directive) return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const FUNCTION_TYPES = /* @__PURE__ */ new Set([
|
|
83
|
+
"FunctionDeclaration",
|
|
84
|
+
"FunctionExpression",
|
|
85
|
+
"ArrowFunctionExpression"
|
|
86
|
+
]);
|
|
87
|
+
function isInlineAction(node, directive) {
|
|
88
|
+
return FUNCTION_TYPES.has(node.type) && node.body?.type === "BlockStatement" && hasDirective(node.body.body, directive);
|
|
89
|
+
}
|
|
90
|
+
function actionName(node, parent) {
|
|
91
|
+
if (node.id?.name) return node.id.name;
|
|
92
|
+
if (parent?.type === "VariableDeclarator" && parent.id?.type === "Identifier") return parent.id.name;
|
|
93
|
+
if (parent?.type === "Property" && parent.key?.type === "Identifier") return parent.key.name;
|
|
94
|
+
if (parent?.type === "JSXExpressionContainer") return null;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/** Exports of a `"use server"` module, refusing what cannot be an action. */
|
|
98
|
+
function collectExports(program, fail, directive) {
|
|
99
|
+
const exports = [];
|
|
100
|
+
const onlyAsync = `A "${directive}" module may export only async functions`;
|
|
101
|
+
const checkFunction = (node, name) => {
|
|
102
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
103
|
+
if (!node.async) fail(`${onlyAsync}: make ${name} async`, node);
|
|
104
|
+
if (node.generator) fail(`${onlyAsync}: ${name} is a generator`, node);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (node.type === "Literal" || node.type === "ObjectExpression" || node.type === "ArrayExpression" || node.type === "TemplateLiteral" || node.type === "ClassExpression") fail(`${onlyAsync}; ${name} is not a function`, node);
|
|
108
|
+
};
|
|
109
|
+
for (const node of program.body ?? []) {
|
|
110
|
+
if (node.type === "ExportAllDeclaration") fail(`"${directive}" modules cannot re-export with \`export *\`: export each action by name`, node);
|
|
111
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
112
|
+
if (node.exportKind === "type") continue;
|
|
113
|
+
if (node.source) fail(`"${directive}" modules cannot re-export from another module: import the actions, then export them`, node);
|
|
114
|
+
const declaration = node.declaration;
|
|
115
|
+
if (declaration?.type === "FunctionDeclaration") {
|
|
116
|
+
checkFunction(declaration, declaration.id.name);
|
|
117
|
+
exports.push({
|
|
118
|
+
exported: declaration.id.name,
|
|
119
|
+
local: declaration.id.name,
|
|
120
|
+
node: declaration
|
|
121
|
+
});
|
|
122
|
+
} else if (declaration?.type === "VariableDeclaration") for (const declarator of declaration.declarations ?? []) {
|
|
123
|
+
if (declarator.id?.type !== "Identifier") fail(`${onlyAsync}; destructured exports are not supported`, declarator);
|
|
124
|
+
if (declarator.init) checkFunction(declarator.init, declarator.id.name);
|
|
125
|
+
exports.push({
|
|
126
|
+
exported: declarator.id.name,
|
|
127
|
+
local: declarator.id.name,
|
|
128
|
+
node: declarator
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else if (declaration?.type === "ClassDeclaration") fail(`${onlyAsync}; ${declaration.id?.name ?? "a class"} is a class`, declaration);
|
|
132
|
+
else if (declaration && !String(declaration.type).startsWith("TS")) fail(onlyAsync, declaration);
|
|
133
|
+
for (const specifier of node.specifiers ?? []) {
|
|
134
|
+
if (specifier.exportKind === "type") continue;
|
|
135
|
+
const exported = specifier.exported?.name ?? specifier.exported?.value;
|
|
136
|
+
exports.push({
|
|
137
|
+
exported,
|
|
138
|
+
local: specifier.local?.name ?? null,
|
|
139
|
+
node: specifier
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
} else if (node.type === "ExportDefaultDeclaration") {
|
|
143
|
+
const declaration = node.declaration;
|
|
144
|
+
checkFunction(declaration, "the default export");
|
|
145
|
+
const local = FUNCTION_TYPES.has(declaration.type) && declaration.id?.name || declaration.type === "Identifier" && declaration.name || null;
|
|
146
|
+
exports.push({
|
|
147
|
+
exported: "default",
|
|
148
|
+
local,
|
|
149
|
+
node
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return exports;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Compiles a module's server actions, or returns null when it has none. Throws an
|
|
157
|
+
* `ActionCompileError` for a mistake, such as an inline action in browser code.
|
|
158
|
+
*/
|
|
159
|
+
function transformActions(options) {
|
|
160
|
+
const { code, id, root, environment } = options;
|
|
161
|
+
const salt = options.salt ?? "";
|
|
162
|
+
const serverRuntime = options.runtime?.server ?? "@farm.js/islx/server";
|
|
163
|
+
const clientRuntime = options.runtime?.client ?? "@farm.js/islx/client";
|
|
164
|
+
const directive = options.directive ?? "use server";
|
|
165
|
+
const moduleDirective = hasModuleDirective(code, directive);
|
|
166
|
+
if (!moduleDirective && !code.includes(directive)) return null;
|
|
167
|
+
const program = options.parse(code);
|
|
168
|
+
const file = id.split("?", 1)[0];
|
|
169
|
+
const relative = path.relative(root, file).split(path.sep).join("/");
|
|
170
|
+
const fail = (message, node) => {
|
|
171
|
+
throw new ActionCompileError(message, file, code, node.start);
|
|
172
|
+
};
|
|
173
|
+
const actionId = (name) => getActionId(root, file, name, salt);
|
|
174
|
+
const label = (name) => `${name} (${relative})`;
|
|
175
|
+
if (moduleDirective && environment === "client") {
|
|
176
|
+
const exports = collectExports(program, fail, directive);
|
|
177
|
+
const lines = [`import { createServerReference as $$islx_ref } from ${JSON.stringify(clientRuntime)};`];
|
|
178
|
+
for (const entry of exports) {
|
|
179
|
+
const reference = `$$islx_ref(${JSON.stringify(actionId(entry.exported))}, undefined, ${JSON.stringify(entry.exported === "default" ? "default" : entry.exported)})`;
|
|
180
|
+
lines.push(entry.exported === "default" ? `export default ${reference};` : `export const ${entry.exported} = ${reference};`);
|
|
181
|
+
}
|
|
182
|
+
const output = new MagicString(code);
|
|
183
|
+
output.overwrite(0, code.length, `${lines.join("\n")}\n`);
|
|
184
|
+
return {
|
|
185
|
+
code: output.toString(),
|
|
186
|
+
map: output.generateMap({
|
|
187
|
+
hires: true,
|
|
188
|
+
source: file
|
|
189
|
+
})
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const { map, scope: moduleScope } = analyze(program);
|
|
193
|
+
const scopes = [moduleScope];
|
|
194
|
+
const scopeNodes = [program];
|
|
195
|
+
const inline = [];
|
|
196
|
+
const active = [];
|
|
197
|
+
let functionDepth = 0;
|
|
198
|
+
const ownFunctionDepth = [];
|
|
199
|
+
walk(program, {
|
|
200
|
+
enter(rawNode, rawParent) {
|
|
201
|
+
const node = rawNode;
|
|
202
|
+
const parent = rawParent;
|
|
203
|
+
if (typeof node.type === "string" && node.type.startsWith("TS")) {
|
|
204
|
+
this.skip();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const scope = node === program ? void 0 : map.get(node);
|
|
208
|
+
if (scope) {
|
|
209
|
+
scopes.push(scope);
|
|
210
|
+
scopeNodes.push(node);
|
|
211
|
+
}
|
|
212
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
213
|
+
if (isInlineAction(node, directive) && !(moduleDirective && functionDepth === 0)) {
|
|
214
|
+
if (environment === "client") fail(`An inline "${directive}" function can only be written in server code. Move it to a "${directive}" module and import it here`, node);
|
|
215
|
+
if (!node.async) fail("A server action must be an async function", node);
|
|
216
|
+
if (node.generator) fail("A server action cannot be a generator", node);
|
|
217
|
+
const action = {
|
|
218
|
+
node,
|
|
219
|
+
parent,
|
|
220
|
+
nested: functionDepth > 0,
|
|
221
|
+
name: actionName(node, parent),
|
|
222
|
+
captured: [],
|
|
223
|
+
scopeIndex: scopes.length - 1
|
|
224
|
+
};
|
|
225
|
+
inline.push(action);
|
|
226
|
+
active.push(action);
|
|
227
|
+
ownFunctionDepth.push(0);
|
|
228
|
+
} else if (node.type !== "ArrowFunctionExpression") for (let index = 0; index < ownFunctionDepth.length; index++) ownFunctionDepth[index]++;
|
|
229
|
+
functionDepth++;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (active.length === 0) return;
|
|
233
|
+
if (node.type === "ThisExpression" || node.type === "Identifier" && node.name === "arguments" && parent && isReference(node, parent)) {
|
|
234
|
+
for (let index = 0; index < active.length; index++) if (ownFunctionDepth[index] === 0) fail(`A server action cannot use \`${node.type === "ThisExpression" ? "this" : "arguments"}\`: pass what it needs as an argument`, node);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (node.type !== "Identifier" || !parent || !isReference(node, parent)) return;
|
|
238
|
+
if (parent.id === node && FUNCTION_TYPES.has(parent.type)) return;
|
|
239
|
+
const owner = scopes.at(-1).find_owner(node.name);
|
|
240
|
+
if (!owner || owner === moduleScope) return;
|
|
241
|
+
const ownerIndex = scopes.lastIndexOf(owner);
|
|
242
|
+
for (const action of active) if (ownerIndex > 0 && ownerIndex < action.scopeIndex && !action.captured.includes(node.name)) {
|
|
243
|
+
if (action.node.type === "FunctionDeclaration" && action.name === node.name) fail(`The server action ${node.name} refers to itself. To call it again, move it to a "${directive}" module`, node);
|
|
244
|
+
action.captured.push(node.name);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
leave(rawNode) {
|
|
248
|
+
const node = rawNode;
|
|
249
|
+
if (scopeNodes.at(-1) === node && node !== program) {
|
|
250
|
+
scopes.pop();
|
|
251
|
+
scopeNodes.pop();
|
|
252
|
+
}
|
|
253
|
+
if (FUNCTION_TYPES.has(node.type)) {
|
|
254
|
+
functionDepth--;
|
|
255
|
+
if (active.at(-1)?.node === node) {
|
|
256
|
+
active.pop();
|
|
257
|
+
ownFunctionDepth.pop();
|
|
258
|
+
} else if (node.type !== "ArrowFunctionExpression") for (let index = 0; index < ownFunctionDepth.length; index++) ownFunctionDepth[index]--;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
if (inline.length === 0 && !moduleDirective) return null;
|
|
263
|
+
const output = new MagicString(code);
|
|
264
|
+
const footer = [];
|
|
265
|
+
let usesBind = false;
|
|
266
|
+
let usesInline = false;
|
|
267
|
+
let usesReference = false;
|
|
268
|
+
const numbered = [...inline].sort((a, b) => a.node.start - b.node.start);
|
|
269
|
+
const ids = new Map(numbered.map((action, index) => [action, index]));
|
|
270
|
+
for (const action of [...inline].sort((a, b) => b.node.start - a.node.start)) {
|
|
271
|
+
const index = ids.get(action);
|
|
272
|
+
const name = action.name ?? `action ${index + 1}`;
|
|
273
|
+
const idValue = actionId(`inline:${index}`);
|
|
274
|
+
const { node } = action;
|
|
275
|
+
if (!action.nested) {
|
|
276
|
+
usesReference = true;
|
|
277
|
+
if (node.type === "FunctionDeclaration") footer.push(`$$islx_ref(${node.id.name}, ${JSON.stringify(idValue)}, ${JSON.stringify(label(name))});`);
|
|
278
|
+
else {
|
|
279
|
+
output.prependLeft(node.start, "$$islx_ref(");
|
|
280
|
+
output.appendRight(node.end, `, ${JSON.stringify(idValue)}, ${JSON.stringify(label(name))})`);
|
|
281
|
+
}
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
usesBind = true;
|
|
285
|
+
usesInline = true;
|
|
286
|
+
const hoisted = `$$islx_action_${index}`;
|
|
287
|
+
const params = node.params.map((param) => output.slice(param.start, param.end));
|
|
288
|
+
const captured = action.captured.length > 0 ? `[${action.captured.join(", ")}]` : "$$islx_captured";
|
|
289
|
+
const body = output.slice(node.body.start + 1, node.body.end - 1);
|
|
290
|
+
footer.push(`async function ${hoisted}(${[captured, ...params].join(", ")}) {${body}}`, `$$islx_inline(${hoisted}, ${JSON.stringify(idValue)}, ${action.captured.length}, ${JSON.stringify(label(name))});`);
|
|
291
|
+
const reference = `$$islx_bind(${JSON.stringify(idValue)}, () => [${action.captured.join(", ")}], ${JSON.stringify(action.captured)})`;
|
|
292
|
+
if (node.type === "FunctionDeclaration") {
|
|
293
|
+
const block = action.parent;
|
|
294
|
+
if (!block || block.type !== "BlockStatement" && block.type !== "StaticBlock") fail("Declare this server action directly inside a function body", node);
|
|
295
|
+
output.remove(node.start, node.end);
|
|
296
|
+
output.appendRight(block.start + 1, `const ${node.id.name} = ${reference};`);
|
|
297
|
+
} else output.overwrite(node.start, node.end, reference);
|
|
298
|
+
}
|
|
299
|
+
if (moduleDirective) {
|
|
300
|
+
usesReference = true;
|
|
301
|
+
for (const entry of collectExports(program, fail, directive)) {
|
|
302
|
+
if (!entry.local) {
|
|
303
|
+
const declaration = entry.node.declaration;
|
|
304
|
+
output.overwrite(entry.node.start, declaration.start, "const $$islx_default = ");
|
|
305
|
+
footer.push("export { $$islx_default as default };");
|
|
306
|
+
entry.local = "$$islx_default";
|
|
307
|
+
}
|
|
308
|
+
footer.push(`$$islx_ref(${entry.local}, ${JSON.stringify(actionId(entry.exported))}, ${JSON.stringify(label(entry.exported))});`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const imports = [
|
|
312
|
+
usesBind && "bindInlineAction as $$islx_bind",
|
|
313
|
+
usesInline && "registerInlineAction as $$islx_inline",
|
|
314
|
+
usesReference && "registerServerReference as $$islx_ref"
|
|
315
|
+
].filter(Boolean);
|
|
316
|
+
output.prepend(`import { ${imports.join(", ")} } from ${JSON.stringify(serverRuntime)};\n`);
|
|
317
|
+
output.append(`\n${footer.join("\n")}\n`);
|
|
318
|
+
return {
|
|
319
|
+
code: output.toString(),
|
|
320
|
+
map: output.generateMap({
|
|
321
|
+
hires: true,
|
|
322
|
+
source: file
|
|
323
|
+
})
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
export { transformActions as i, getActionId as n, hasModuleDirective as r, ActionCompileError as t };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// src/server/build.ts
|
|
2
|
+
const BUILD_KEY = Symbol.for("islx.build");
|
|
3
|
+
function setBuildDefaults(defaults) {
|
|
4
|
+
globalThis[BUILD_KEY] = defaults;
|
|
5
|
+
}
|
|
6
|
+
function getBuildDefaults() {
|
|
7
|
+
return globalThis[BUILD_KEY] ?? {};
|
|
8
|
+
}
|
|
9
|
+
export { setBuildDefaults as n, getBuildDefaults as t };
|