@solidjs/web 2.0.0-rc.3 → 2.0.0-rc.4
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/dist/dev.cjs +347 -11
- package/dist/dev.js +347 -13
- package/dist/server.cjs +5 -4
- package/dist/server.js +5 -4
- package/dist/web.cjs +338 -11
- package/dist/web.js +338 -13
- package/package.json +2 -2
- package/server-functions/dist/client.cjs +126 -19
- package/server-functions/dist/client.js +123 -19
- package/server-functions/dist/server.cjs +156 -28
- package/server-functions/dist/server.dev.cjs +156 -28
- package/server-functions/dist/server.dev.js +153 -28
- package/server-functions/dist/server.js +153 -28
- package/types/client.d.ts +4 -2
- package/types/index.d.ts +1 -0
- package/types/patch-driver.d.ts +3 -0
- package/types/response.d.ts +9 -1
- package/types/server-functions/client.d.ts +37 -6
- package/types/server-functions/registry.d.ts +38 -1
- package/types/server-functions/server.d.ts +9 -6
- package/types/server-functions/shared.d.ts +46 -3
- package/types-cjs/client.d.cts +4 -2
- package/types-cjs/index.d.cts +1 -0
- package/types-cjs/patch-driver.d.cts +3 -0
- package/types-cjs/response.d.cts +9 -1
- package/types-cjs/server-functions/client.d.cts +37 -6
- package/types-cjs/server-functions/registry.d.cts +38 -1
- package/types-cjs/server-functions/server.d.cts +9 -6
- package/types-cjs/server-functions/shared.d.cts +46 -3
|
@@ -26,6 +26,32 @@ function withMeta(fn, meta) {
|
|
|
26
26
|
Object.assign(metadata, meta);
|
|
27
27
|
return fn;
|
|
28
28
|
}
|
|
29
|
+
const SERVER_FUNCTION_INVOKE = Symbol.for("solid.ServerFunctionInvoke");
|
|
30
|
+
const INVOKE_OPTION_REDIRECTS = {
|
|
31
|
+
headers: "Session-dynamic headers belong to the prepareRequest hook, declaration metadata to " + "withMeta(fn, meta), and data belongs in the arguments, where it is serialized, typed, " + "and part of the cache key.",
|
|
32
|
+
method: "The method is declaration-scoped: declare the function with GET(fn).",
|
|
33
|
+
body: "The arguments are the body: pass them in the args array.",
|
|
34
|
+
timeout: "Compose timeouts through `signal` with AbortSignal.timeout(ms) (and AbortSignal.any to " + "combine it with a caller signal)."
|
|
35
|
+
};
|
|
36
|
+
function invoke(fn, options, ...args) {
|
|
37
|
+
const channel = typeof fn === "function" && fn[SERVER_FUNCTION_INVOKE];
|
|
38
|
+
if (!channel) {
|
|
39
|
+
throw new Error(isServerFunction(fn) ? "invoke: this wrapper does not forward the invocation channel " + "(SERVER_FUNCTION_INVOKE). Wrappers that share calls (caches, channels) opt in " + "deliberately — a caller's signal cannot own a wire other callers share. Invoke " + "the underlying reference directly, or use the wrapper's own per-call idioms." : "invoke expects a server function reference (or a wrapper that forwards its " + "invocation channel). Per-call options apply at the transport; for a data " + "layer's calls, use its per-call options instead.");
|
|
40
|
+
}
|
|
41
|
+
if (options === null || typeof options !== "object") {
|
|
42
|
+
throw new Error("invoke's second argument is the invocation options bag: invoke(fn, { signal }, ...args)");
|
|
43
|
+
}
|
|
44
|
+
const picked = {};
|
|
45
|
+
for (const key in options) {
|
|
46
|
+
if (key !== "signal" && key !== "keepalive" && key !== "priority") {
|
|
47
|
+
throw new Error(`\`${key}\` is not an invocation option. ` + (INVOKE_OPTION_REDIRECTS[key] || "Options here are strictly invocation-scoped (they vary between calls of the " + "same function): signal, keepalive, priority."));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (options.signal !== undefined) picked.signal = options.signal;
|
|
51
|
+
if (options.keepalive !== undefined) picked.keepalive = options.keepalive;
|
|
52
|
+
if (options.priority !== undefined) picked.priority = options.priority;
|
|
53
|
+
return channel(args, picked);
|
|
54
|
+
}
|
|
29
55
|
const LIVE_SOURCE = Symbol.for("solid.LiveSource");
|
|
30
56
|
const SERVER_FUNCTION_RPC = Symbol.for("solid.ServerFunctionRPC");
|
|
31
57
|
function provideServerFunctionRPC(rpc) {
|
|
@@ -90,7 +116,23 @@ function subscribeFlightData(consumer) {
|
|
|
90
116
|
return () => {
|
|
91
117
|
};
|
|
92
118
|
}
|
|
93
|
-
|
|
119
|
+
function serverFunctionAddress(endpoint, id) {
|
|
120
|
+
const mount = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
|
|
121
|
+
return `${mount}/${encodeURIComponent(id)}`;
|
|
122
|
+
}
|
|
123
|
+
function parseServerFunctionAddress(pathname, endpoint) {
|
|
124
|
+
const mount = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
|
|
125
|
+
if (!pathname.startsWith(mount)) return null;
|
|
126
|
+
const rest = pathname.slice(mount.length);
|
|
127
|
+
if (!rest.startsWith("/")) return null;
|
|
128
|
+
const segment = rest.slice(1);
|
|
129
|
+
if (!segment || segment.includes("/")) return null;
|
|
130
|
+
try {
|
|
131
|
+
return decodeURIComponent(segment);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
94
136
|
const ERROR_HEADER = "X-Server-Function-Error";
|
|
95
137
|
const ERROR_HEADER_MARKER = "=?1?";
|
|
96
138
|
const NEEDS_ENCODING = /[^\x20-\x7e\xa0-\xff]/;
|
|
@@ -129,7 +171,8 @@ const BodyFormat = {
|
|
|
129
171
|
File: "5",
|
|
130
172
|
ArrayBuffer: "6",
|
|
131
173
|
Uint8Array: "7",
|
|
132
|
-
Json: "8"
|
|
174
|
+
Json: "8",
|
|
175
|
+
Void: "9"
|
|
133
176
|
};
|
|
134
177
|
const JSON_SAFE_DEPTH_LIMIT = 4096;
|
|
135
178
|
const EXIT = {};
|
|
@@ -549,6 +592,9 @@ function getServerFunction(id) {
|
|
|
549
592
|
throw new Error("invalid server function: " + id);
|
|
550
593
|
}
|
|
551
594
|
function registerServerReference(id, fn, name) {
|
|
595
|
+
if (typeof fn !== "function") {
|
|
596
|
+
throw new Error(`Server function${name ? ` \`${name}\`` : ""} (${id}) is not a function: a module-level ` + `"use server" export must evaluate to a server function (got ${fn === null ? "null" : typeof fn}). Move non-function exports out of the directive module.`);
|
|
597
|
+
}
|
|
552
598
|
registerServerFunction(id, fn);
|
|
553
599
|
return {
|
|
554
600
|
id,
|
|
@@ -556,6 +602,33 @@ function registerServerReference(id, fn, name) {
|
|
|
556
602
|
name
|
|
557
603
|
};
|
|
558
604
|
}
|
|
605
|
+
function inProcessInvoker(call) {
|
|
606
|
+
return (args, options) => {
|
|
607
|
+
const signal = options && options.signal;
|
|
608
|
+
if (!signal) return call(...args);
|
|
609
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
610
|
+
return new Promise((resolve, reject) => {
|
|
611
|
+
const onAbort = () => reject(signal.reason);
|
|
612
|
+
signal.addEventListener("abort", onAbort, {
|
|
613
|
+
once: true
|
|
614
|
+
});
|
|
615
|
+
let result;
|
|
616
|
+
try {
|
|
617
|
+
result = call(...args);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
signal.removeEventListener("abort", onAbort);
|
|
620
|
+
return reject(error);
|
|
621
|
+
}
|
|
622
|
+
Promise.resolve(result).then(value => {
|
|
623
|
+
signal.removeEventListener("abort", onAbort);
|
|
624
|
+
resolve(value);
|
|
625
|
+
}, error => {
|
|
626
|
+
signal.removeEventListener("abort", onAbort);
|
|
627
|
+
reject(error);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
};
|
|
631
|
+
}
|
|
559
632
|
function createServerReference({
|
|
560
633
|
id,
|
|
561
634
|
fn,
|
|
@@ -566,13 +639,15 @@ function createServerReference({
|
|
|
566
639
|
const metadata = name === undefined ? {} : {
|
|
567
640
|
name
|
|
568
641
|
};
|
|
569
|
-
|
|
642
|
+
const invokeChannel = inProcessInvoker((...args) => proxy(...args));
|
|
643
|
+
const proxy = new Proxy(fn, {
|
|
570
644
|
get(target, prop) {
|
|
571
645
|
if (prop === "id") return id;
|
|
572
646
|
if (prop === "url") {
|
|
573
|
-
return
|
|
647
|
+
return serverFunctionAddress(config.endpoint, id);
|
|
574
648
|
}
|
|
575
649
|
if (prop === SERVER_FUNCTION_METADATA) return metadata;
|
|
650
|
+
if (prop === SERVER_FUNCTION_INVOKE) return invokeChannel;
|
|
576
651
|
return target[prop];
|
|
577
652
|
},
|
|
578
653
|
apply(target, thisArg, args) {
|
|
@@ -609,6 +684,7 @@ function createServerReference({
|
|
|
609
684
|
}) : result;
|
|
610
685
|
}
|
|
611
686
|
});
|
|
687
|
+
return proxy;
|
|
612
688
|
}
|
|
613
689
|
function GET(fn) {
|
|
614
690
|
if (!isServerFunction(fn) || typeof fn.id !== "string") {
|
|
@@ -635,6 +711,7 @@ function live(fn) {
|
|
|
635
711
|
return result;
|
|
636
712
|
};
|
|
637
713
|
wrapped[SERVER_FUNCTION_METADATA] = metadata;
|
|
714
|
+
wrapped[SERVER_FUNCTION_INVOKE] = inProcessInvoker(wrapped);
|
|
638
715
|
wrapped.id = fn.id;
|
|
639
716
|
Object.defineProperty(wrapped, "url", {
|
|
640
717
|
get: () => fn.url,
|
|
@@ -648,24 +725,23 @@ function getServerFunctionInvocation() {
|
|
|
648
725
|
function getEventServerFunctionInvocation(event) {
|
|
649
726
|
return event && INVOCATIONS.get(event);
|
|
650
727
|
}
|
|
651
|
-
function resolveFunctionId(
|
|
652
|
-
|
|
653
|
-
if (reference) {
|
|
654
|
-
return reference.split("#")[0];
|
|
655
|
-
}
|
|
656
|
-
return url.searchParams.get("id");
|
|
728
|
+
function resolveFunctionId(url) {
|
|
729
|
+
return parseServerFunctionAddress(url.pathname, config.endpoint);
|
|
657
730
|
}
|
|
658
731
|
async function parseArguments(request, url, instance, codec) {
|
|
659
732
|
const parsed = [];
|
|
660
733
|
const bodyFormat = request.method === "POST" ? request.headers.get(BODY_FORMAT_HEADER) : null;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
734
|
+
const args = url.searchParams.get("args");
|
|
735
|
+
if (args && (!instance || request.method === "GET" || bodyFormat !== BodyFormat.Serialized)) {
|
|
736
|
+
const result = args.startsWith(";0x") ? await deserializeString(args, codec) : JSON.parse(args);
|
|
737
|
+
if (!Array.isArray(result)) {
|
|
738
|
+
throw new TypeError("Server function arguments must encode an array");
|
|
739
|
+
}
|
|
740
|
+
for (const arg of result) {
|
|
741
|
+
parsed.push(arg);
|
|
668
742
|
}
|
|
743
|
+
} else if (!args && url.search && (request.method === "GET" || request.method === "HEAD")) {
|
|
744
|
+
parsed.push(url.searchParams);
|
|
669
745
|
}
|
|
670
746
|
if (request.method === "POST" && request.body !== null) {
|
|
671
747
|
const decoded = await extractBody(request.clone(), codec);
|
|
@@ -920,6 +996,7 @@ function encodeResult(value, headers, status, codec, signal) {
|
|
|
920
996
|
});
|
|
921
997
|
}
|
|
922
998
|
if (value === undefined) {
|
|
999
|
+
headers.set(BODY_FORMAT_HEADER, BodyFormat.Void);
|
|
923
1000
|
return new Response(null, {
|
|
924
1001
|
status,
|
|
925
1002
|
headers
|
|
@@ -955,6 +1032,17 @@ function sanitizeServerError(value) {
|
|
|
955
1032
|
function observeServerFunctionCalls() {
|
|
956
1033
|
return () => {};
|
|
957
1034
|
}
|
|
1035
|
+
function serverFunctionUrl(id, boundArgs) {
|
|
1036
|
+
const address = serverFunctionAddress(config.endpoint, id);
|
|
1037
|
+
if (!boundArgs || !boundArgs.length) return address;
|
|
1038
|
+
if (!isJSONSafe(boundArgs)) {
|
|
1039
|
+
throw new Error("Bound arguments in an action url must be JSON-safe: the server reads them the way it " + "reads a form post's, and that convention has no codec. Pass the value through the " + "function's body, or call the reference instead of rendering a url for it.");
|
|
1040
|
+
}
|
|
1041
|
+
return `${address}?args=${encodeURIComponent(JSON.stringify(boundArgs))}`;
|
|
1042
|
+
}
|
|
1043
|
+
function parseServerFunctionUrl(url) {
|
|
1044
|
+
return parseServerFunctionAddress(new URL(url, "http://localhost").pathname, config.endpoint);
|
|
1045
|
+
}
|
|
958
1046
|
async function matchesOrigin(origin, request, matcher) {
|
|
959
1047
|
if (matcher === undefined) return origin === new URL(request.url).origin;
|
|
960
1048
|
if (typeof matcher === "function") return !!(await matcher(origin, request));
|
|
@@ -1012,18 +1100,20 @@ function forbiddenResponse() {
|
|
|
1012
1100
|
async function handleServerFunctionRequest(request, options = {}) {
|
|
1013
1101
|
const codec = options.codec !== undefined ? options.codec : getServerFunctionsCodec();
|
|
1014
1102
|
const url = new URL(request.url);
|
|
1103
|
+
const method = request.method;
|
|
1104
|
+
const functionId = resolveFunctionId(url);
|
|
1105
|
+
const declaredRead = (method === "GET" || method === "HEAD") && functionId !== null && METHODS.get(functionId) === "GET";
|
|
1015
1106
|
const csrf = options.csrf !== undefined ? options.csrf : config.csrf;
|
|
1016
|
-
const protectsRequest = csrf !== false;
|
|
1107
|
+
const protectsRequest = csrf !== false && !declaredRead;
|
|
1017
1108
|
if (protectsRequest && !(await allowsServerFunctionRequest(request, csrf === true ? {} : csrf))) {
|
|
1018
|
-
return forbiddenResponse();
|
|
1109
|
+
return finalizeTransportResponse(forbiddenResponse(), method);
|
|
1019
1110
|
}
|
|
1020
1111
|
const instance = request.headers.get(INSTANCE_HEADER);
|
|
1021
|
-
const functionId = resolveFunctionId(request, url);
|
|
1022
1112
|
if (!functionId) {
|
|
1023
1113
|
const response = new Response(DEV ? "Server function not found" : null, {
|
|
1024
1114
|
status: 404
|
|
1025
1115
|
});
|
|
1026
|
-
return protectsRequest ? withCSRFVary(response) : response;
|
|
1116
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1027
1117
|
}
|
|
1028
1118
|
let serverFunction;
|
|
1029
1119
|
try {
|
|
@@ -1032,16 +1122,16 @@ async function handleServerFunctionRequest(request, options = {}) {
|
|
|
1032
1122
|
const response = new Response(DEV ? `Unknown server function: ${functionId}` : null, {
|
|
1033
1123
|
status: 404
|
|
1034
1124
|
});
|
|
1035
|
-
return protectsRequest ? withCSRFVary(response) : response;
|
|
1125
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1036
1126
|
}
|
|
1037
|
-
if (
|
|
1127
|
+
if (method !== "POST" && !declaredRead) {
|
|
1038
1128
|
const response = new Response(DEV ? `Method not allowed for server function: ${functionId}` : null, {
|
|
1039
1129
|
status: 405,
|
|
1040
1130
|
headers: {
|
|
1041
|
-
Allow: "POST"
|
|
1131
|
+
Allow: METHODS.get(functionId) === "GET" ? "POST, GET, HEAD" : "POST"
|
|
1042
1132
|
}
|
|
1043
1133
|
});
|
|
1044
|
-
return protectsRequest ? withCSRFVary(response) : response;
|
|
1134
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1045
1135
|
}
|
|
1046
1136
|
const event = options.createEvent ? options.createEvent(request) : {
|
|
1047
1137
|
request,
|
|
@@ -1054,7 +1144,15 @@ async function handleServerFunctionRequest(request, options = {}) {
|
|
|
1054
1144
|
const transformFlightResult = options.transformFlightResult !== undefined ? options.transformFlightResult : config.transformFlightResult;
|
|
1055
1145
|
const handleNoJS = options.handleNoJS !== undefined ? options.handleNoJS : config.handleNoJS !== undefined ? config.handleNoJS : isFormPost(request) ? defaultNoJSHandler || (defaultNoJSHandler = createNoJSHandler()) : undefined;
|
|
1056
1146
|
const collectsFlight = !!(flightHook && instance && request.headers.has(SINGLE_FLIGHT_HEADER));
|
|
1057
|
-
|
|
1147
|
+
let parsed;
|
|
1148
|
+
try {
|
|
1149
|
+
parsed = await parseArguments(request, url, instance, codec);
|
|
1150
|
+
} catch {
|
|
1151
|
+
const response = new Response(DEV ? "Malformed server function arguments" : null, {
|
|
1152
|
+
status: 400
|
|
1153
|
+
});
|
|
1154
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1155
|
+
}
|
|
1058
1156
|
const flightContext = {
|
|
1059
1157
|
id: functionId,
|
|
1060
1158
|
args: parsed,
|
|
@@ -1201,7 +1299,34 @@ async function handleServerFunctionRequest(request, options = {}) {
|
|
|
1201
1299
|
}
|
|
1202
1300
|
};
|
|
1203
1301
|
const response = commitEventResponse(await dispatch(), event);
|
|
1204
|
-
return protectsRequest ? withCSRFVary(response) : response;
|
|
1302
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1303
|
+
}
|
|
1304
|
+
function finalizeTransportResponse(response, method) {
|
|
1305
|
+
const stripBody = method === "HEAD" && response.body !== null;
|
|
1306
|
+
if (stripBody || !response.headers.has("Cache-Control")) {
|
|
1307
|
+
try {
|
|
1308
|
+
if (!response.headers.has("Cache-Control")) {
|
|
1309
|
+
response.headers.set("Cache-Control", "no-store");
|
|
1310
|
+
}
|
|
1311
|
+
if (!stripBody) return response;
|
|
1312
|
+
response.body.cancel().catch(() => {});
|
|
1313
|
+
return new Response(null, {
|
|
1314
|
+
status: response.status,
|
|
1315
|
+
statusText: response.statusText,
|
|
1316
|
+
headers: response.headers
|
|
1317
|
+
});
|
|
1318
|
+
} catch {
|
|
1319
|
+
const headers = new Headers(response.headers);
|
|
1320
|
+
if (!headers.has("Cache-Control")) headers.set("Cache-Control", "no-store");
|
|
1321
|
+
if (stripBody) response.body.cancel().catch(() => {});
|
|
1322
|
+
return new Response(stripBody ? null : response.body, {
|
|
1323
|
+
status: response.status,
|
|
1324
|
+
statusText: response.statusText,
|
|
1325
|
+
headers
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return response;
|
|
1205
1330
|
}
|
|
1206
1331
|
|
|
1207
|
-
export { ERROR_HEADER, FLASH_COOKIE,
|
|
1332
|
+
export { ERROR_HEADER, FLASH_COOKIE, GENERIC_SERVER_ERROR_MESSAGE, GET, INSTANCE_HEADER, SERVER_FUNCTION_INVOKE, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsServer, createNoJSHandler, createServerReference, decodeErrorHeaderValue, decodeFlashCookie, decodeResponse, decodeResponsePayload, encodeErrorHeaderValue, encodeFlashCookie, foldSetCookies, getEventServerFunctionInvocation, getServerFunction, getServerFunctionInvocation, getServerFunctionMetadata, handleServerFunctionRequest, hasFlashCookie, invoke, isServerFunction, live, observeServerFunctionCalls, parseServerFunctionUrl, registerServerFunction, registerServerReference, sanitizeServerError, serializeResponseStream, serverFunctionUrl, setServerFunctionsDev, subscribeFlightData, withMeta };
|
|
@@ -26,6 +26,32 @@ function withMeta(fn, meta) {
|
|
|
26
26
|
Object.assign(metadata, meta);
|
|
27
27
|
return fn;
|
|
28
28
|
}
|
|
29
|
+
const SERVER_FUNCTION_INVOKE = Symbol.for("solid.ServerFunctionInvoke");
|
|
30
|
+
const INVOKE_OPTION_REDIRECTS = {
|
|
31
|
+
headers: "Session-dynamic headers belong to the prepareRequest hook, declaration metadata to " + "withMeta(fn, meta), and data belongs in the arguments, where it is serialized, typed, " + "and part of the cache key.",
|
|
32
|
+
method: "The method is declaration-scoped: declare the function with GET(fn).",
|
|
33
|
+
body: "The arguments are the body: pass them in the args array.",
|
|
34
|
+
timeout: "Compose timeouts through `signal` with AbortSignal.timeout(ms) (and AbortSignal.any to " + "combine it with a caller signal)."
|
|
35
|
+
};
|
|
36
|
+
function invoke(fn, options, ...args) {
|
|
37
|
+
const channel = typeof fn === "function" && fn[SERVER_FUNCTION_INVOKE];
|
|
38
|
+
if (!channel) {
|
|
39
|
+
throw new Error(isServerFunction(fn) ? "invoke: this wrapper does not forward the invocation channel " + "(SERVER_FUNCTION_INVOKE). Wrappers that share calls (caches, channels) opt in " + "deliberately — a caller's signal cannot own a wire other callers share. Invoke " + "the underlying reference directly, or use the wrapper's own per-call idioms." : "invoke expects a server function reference (or a wrapper that forwards its " + "invocation channel). Per-call options apply at the transport; for a data " + "layer's calls, use its per-call options instead.");
|
|
40
|
+
}
|
|
41
|
+
if (options === null || typeof options !== "object") {
|
|
42
|
+
throw new Error("invoke's second argument is the invocation options bag: invoke(fn, { signal }, ...args)");
|
|
43
|
+
}
|
|
44
|
+
const picked = {};
|
|
45
|
+
for (const key in options) {
|
|
46
|
+
if (key !== "signal" && key !== "keepalive" && key !== "priority") {
|
|
47
|
+
throw new Error(`\`${key}\` is not an invocation option. ` + (INVOKE_OPTION_REDIRECTS[key] || "Options here are strictly invocation-scoped (they vary between calls of the " + "same function): signal, keepalive, priority."));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (options.signal !== undefined) picked.signal = options.signal;
|
|
51
|
+
if (options.keepalive !== undefined) picked.keepalive = options.keepalive;
|
|
52
|
+
if (options.priority !== undefined) picked.priority = options.priority;
|
|
53
|
+
return channel(args, picked);
|
|
54
|
+
}
|
|
29
55
|
const LIVE_SOURCE = Symbol.for("solid.LiveSource");
|
|
30
56
|
const SERVER_FUNCTION_RPC = Symbol.for("solid.ServerFunctionRPC");
|
|
31
57
|
function provideServerFunctionRPC(rpc) {
|
|
@@ -90,7 +116,23 @@ function subscribeFlightData(consumer) {
|
|
|
90
116
|
return () => {
|
|
91
117
|
};
|
|
92
118
|
}
|
|
93
|
-
|
|
119
|
+
function serverFunctionAddress(endpoint, id) {
|
|
120
|
+
const mount = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
|
|
121
|
+
return `${mount}/${encodeURIComponent(id)}`;
|
|
122
|
+
}
|
|
123
|
+
function parseServerFunctionAddress(pathname, endpoint) {
|
|
124
|
+
const mount = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
|
|
125
|
+
if (!pathname.startsWith(mount)) return null;
|
|
126
|
+
const rest = pathname.slice(mount.length);
|
|
127
|
+
if (!rest.startsWith("/")) return null;
|
|
128
|
+
const segment = rest.slice(1);
|
|
129
|
+
if (!segment || segment.includes("/")) return null;
|
|
130
|
+
try {
|
|
131
|
+
return decodeURIComponent(segment);
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
94
136
|
const ERROR_HEADER = "X-Server-Function-Error";
|
|
95
137
|
const ERROR_HEADER_MARKER = "=?1?";
|
|
96
138
|
const NEEDS_ENCODING = /[^\x20-\x7e\xa0-\xff]/;
|
|
@@ -129,7 +171,8 @@ const BodyFormat = {
|
|
|
129
171
|
File: "5",
|
|
130
172
|
ArrayBuffer: "6",
|
|
131
173
|
Uint8Array: "7",
|
|
132
|
-
Json: "8"
|
|
174
|
+
Json: "8",
|
|
175
|
+
Void: "9"
|
|
133
176
|
};
|
|
134
177
|
const JSON_SAFE_DEPTH_LIMIT = 4096;
|
|
135
178
|
const EXIT = {};
|
|
@@ -549,6 +592,9 @@ function getServerFunction(id) {
|
|
|
549
592
|
throw new Error("invalid server function: " + id);
|
|
550
593
|
}
|
|
551
594
|
function registerServerReference(id, fn, name) {
|
|
595
|
+
if (typeof fn !== "function") {
|
|
596
|
+
throw new Error(`Server function${name ? ` \`${name}\`` : ""} (${id}) is not a function: a module-level ` + `"use server" export must evaluate to a server function (got ${fn === null ? "null" : typeof fn}). Move non-function exports out of the directive module.`);
|
|
597
|
+
}
|
|
552
598
|
registerServerFunction(id, fn);
|
|
553
599
|
return {
|
|
554
600
|
id,
|
|
@@ -556,6 +602,33 @@ function registerServerReference(id, fn, name) {
|
|
|
556
602
|
name
|
|
557
603
|
};
|
|
558
604
|
}
|
|
605
|
+
function inProcessInvoker(call) {
|
|
606
|
+
return (args, options) => {
|
|
607
|
+
const signal = options && options.signal;
|
|
608
|
+
if (!signal) return call(...args);
|
|
609
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
610
|
+
return new Promise((resolve, reject) => {
|
|
611
|
+
const onAbort = () => reject(signal.reason);
|
|
612
|
+
signal.addEventListener("abort", onAbort, {
|
|
613
|
+
once: true
|
|
614
|
+
});
|
|
615
|
+
let result;
|
|
616
|
+
try {
|
|
617
|
+
result = call(...args);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
signal.removeEventListener("abort", onAbort);
|
|
620
|
+
return reject(error);
|
|
621
|
+
}
|
|
622
|
+
Promise.resolve(result).then(value => {
|
|
623
|
+
signal.removeEventListener("abort", onAbort);
|
|
624
|
+
resolve(value);
|
|
625
|
+
}, error => {
|
|
626
|
+
signal.removeEventListener("abort", onAbort);
|
|
627
|
+
reject(error);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
};
|
|
631
|
+
}
|
|
559
632
|
function createServerReference({
|
|
560
633
|
id,
|
|
561
634
|
fn,
|
|
@@ -566,13 +639,15 @@ function createServerReference({
|
|
|
566
639
|
const metadata = name === undefined ? {} : {
|
|
567
640
|
name
|
|
568
641
|
};
|
|
569
|
-
|
|
642
|
+
const invokeChannel = inProcessInvoker((...args) => proxy(...args));
|
|
643
|
+
const proxy = new Proxy(fn, {
|
|
570
644
|
get(target, prop) {
|
|
571
645
|
if (prop === "id") return id;
|
|
572
646
|
if (prop === "url") {
|
|
573
|
-
return
|
|
647
|
+
return serverFunctionAddress(config.endpoint, id);
|
|
574
648
|
}
|
|
575
649
|
if (prop === SERVER_FUNCTION_METADATA) return metadata;
|
|
650
|
+
if (prop === SERVER_FUNCTION_INVOKE) return invokeChannel;
|
|
576
651
|
return target[prop];
|
|
577
652
|
},
|
|
578
653
|
apply(target, thisArg, args) {
|
|
@@ -609,6 +684,7 @@ function createServerReference({
|
|
|
609
684
|
}) : result;
|
|
610
685
|
}
|
|
611
686
|
});
|
|
687
|
+
return proxy;
|
|
612
688
|
}
|
|
613
689
|
function GET(fn) {
|
|
614
690
|
if (!isServerFunction(fn) || typeof fn.id !== "string") {
|
|
@@ -635,6 +711,7 @@ function live(fn) {
|
|
|
635
711
|
return result;
|
|
636
712
|
};
|
|
637
713
|
wrapped[SERVER_FUNCTION_METADATA] = metadata;
|
|
714
|
+
wrapped[SERVER_FUNCTION_INVOKE] = inProcessInvoker(wrapped);
|
|
638
715
|
wrapped.id = fn.id;
|
|
639
716
|
Object.defineProperty(wrapped, "url", {
|
|
640
717
|
get: () => fn.url,
|
|
@@ -648,24 +725,23 @@ function getServerFunctionInvocation() {
|
|
|
648
725
|
function getEventServerFunctionInvocation(event) {
|
|
649
726
|
return event && INVOCATIONS.get(event);
|
|
650
727
|
}
|
|
651
|
-
function resolveFunctionId(
|
|
652
|
-
|
|
653
|
-
if (reference) {
|
|
654
|
-
return reference.split("#")[0];
|
|
655
|
-
}
|
|
656
|
-
return url.searchParams.get("id");
|
|
728
|
+
function resolveFunctionId(url) {
|
|
729
|
+
return parseServerFunctionAddress(url.pathname, config.endpoint);
|
|
657
730
|
}
|
|
658
731
|
async function parseArguments(request, url, instance, codec) {
|
|
659
732
|
const parsed = [];
|
|
660
733
|
const bodyFormat = request.method === "POST" ? request.headers.get(BODY_FORMAT_HEADER) : null;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
734
|
+
const args = url.searchParams.get("args");
|
|
735
|
+
if (args && (!instance || request.method === "GET" || bodyFormat !== BodyFormat.Serialized)) {
|
|
736
|
+
const result = args.startsWith(";0x") ? await deserializeString(args, codec) : JSON.parse(args);
|
|
737
|
+
if (!Array.isArray(result)) {
|
|
738
|
+
throw new TypeError("Server function arguments must encode an array");
|
|
739
|
+
}
|
|
740
|
+
for (const arg of result) {
|
|
741
|
+
parsed.push(arg);
|
|
668
742
|
}
|
|
743
|
+
} else if (!args && url.search && (request.method === "GET" || request.method === "HEAD")) {
|
|
744
|
+
parsed.push(url.searchParams);
|
|
669
745
|
}
|
|
670
746
|
if (request.method === "POST" && request.body !== null) {
|
|
671
747
|
const decoded = await extractBody(request.clone(), codec);
|
|
@@ -920,6 +996,7 @@ function encodeResult(value, headers, status, codec, signal) {
|
|
|
920
996
|
});
|
|
921
997
|
}
|
|
922
998
|
if (value === undefined) {
|
|
999
|
+
headers.set(BODY_FORMAT_HEADER, BodyFormat.Void);
|
|
923
1000
|
return new Response(null, {
|
|
924
1001
|
status,
|
|
925
1002
|
headers
|
|
@@ -955,6 +1032,17 @@ function sanitizeServerError(value) {
|
|
|
955
1032
|
function observeServerFunctionCalls() {
|
|
956
1033
|
return () => {};
|
|
957
1034
|
}
|
|
1035
|
+
function serverFunctionUrl(id, boundArgs) {
|
|
1036
|
+
const address = serverFunctionAddress(config.endpoint, id);
|
|
1037
|
+
if (!boundArgs || !boundArgs.length) return address;
|
|
1038
|
+
if (!isJSONSafe(boundArgs)) {
|
|
1039
|
+
throw new Error("Bound arguments in an action url must be JSON-safe: the server reads them the way it " + "reads a form post's, and that convention has no codec. Pass the value through the " + "function's body, or call the reference instead of rendering a url for it.");
|
|
1040
|
+
}
|
|
1041
|
+
return `${address}?args=${encodeURIComponent(JSON.stringify(boundArgs))}`;
|
|
1042
|
+
}
|
|
1043
|
+
function parseServerFunctionUrl(url) {
|
|
1044
|
+
return parseServerFunctionAddress(new URL(url, "http://localhost").pathname, config.endpoint);
|
|
1045
|
+
}
|
|
958
1046
|
async function matchesOrigin(origin, request, matcher) {
|
|
959
1047
|
if (matcher === undefined) return origin === new URL(request.url).origin;
|
|
960
1048
|
if (typeof matcher === "function") return !!(await matcher(origin, request));
|
|
@@ -1012,18 +1100,20 @@ function forbiddenResponse() {
|
|
|
1012
1100
|
async function handleServerFunctionRequest(request, options = {}) {
|
|
1013
1101
|
const codec = options.codec !== undefined ? options.codec : getServerFunctionsCodec();
|
|
1014
1102
|
const url = new URL(request.url);
|
|
1103
|
+
const method = request.method;
|
|
1104
|
+
const functionId = resolveFunctionId(url);
|
|
1105
|
+
const declaredRead = (method === "GET" || method === "HEAD") && functionId !== null && METHODS.get(functionId) === "GET";
|
|
1015
1106
|
const csrf = options.csrf !== undefined ? options.csrf : config.csrf;
|
|
1016
|
-
const protectsRequest = csrf !== false;
|
|
1107
|
+
const protectsRequest = csrf !== false && !declaredRead;
|
|
1017
1108
|
if (protectsRequest && !(await allowsServerFunctionRequest(request, csrf === true ? {} : csrf))) {
|
|
1018
|
-
return forbiddenResponse();
|
|
1109
|
+
return finalizeTransportResponse(forbiddenResponse(), method);
|
|
1019
1110
|
}
|
|
1020
1111
|
const instance = request.headers.get(INSTANCE_HEADER);
|
|
1021
|
-
const functionId = resolveFunctionId(request, url);
|
|
1022
1112
|
if (!functionId) {
|
|
1023
1113
|
const response = new Response(DEV ? "Server function not found" : null, {
|
|
1024
1114
|
status: 404
|
|
1025
1115
|
});
|
|
1026
|
-
return protectsRequest ? withCSRFVary(response) : response;
|
|
1116
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1027
1117
|
}
|
|
1028
1118
|
let serverFunction;
|
|
1029
1119
|
try {
|
|
@@ -1032,16 +1122,16 @@ async function handleServerFunctionRequest(request, options = {}) {
|
|
|
1032
1122
|
const response = new Response(DEV ? `Unknown server function: ${functionId}` : null, {
|
|
1033
1123
|
status: 404
|
|
1034
1124
|
});
|
|
1035
|
-
return protectsRequest ? withCSRFVary(response) : response;
|
|
1125
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1036
1126
|
}
|
|
1037
|
-
if (
|
|
1127
|
+
if (method !== "POST" && !declaredRead) {
|
|
1038
1128
|
const response = new Response(DEV ? `Method not allowed for server function: ${functionId}` : null, {
|
|
1039
1129
|
status: 405,
|
|
1040
1130
|
headers: {
|
|
1041
|
-
Allow: "POST"
|
|
1131
|
+
Allow: METHODS.get(functionId) === "GET" ? "POST, GET, HEAD" : "POST"
|
|
1042
1132
|
}
|
|
1043
1133
|
});
|
|
1044
|
-
return protectsRequest ? withCSRFVary(response) : response;
|
|
1134
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1045
1135
|
}
|
|
1046
1136
|
const event = options.createEvent ? options.createEvent(request) : {
|
|
1047
1137
|
request,
|
|
@@ -1054,7 +1144,15 @@ async function handleServerFunctionRequest(request, options = {}) {
|
|
|
1054
1144
|
const transformFlightResult = options.transformFlightResult !== undefined ? options.transformFlightResult : config.transformFlightResult;
|
|
1055
1145
|
const handleNoJS = options.handleNoJS !== undefined ? options.handleNoJS : config.handleNoJS !== undefined ? config.handleNoJS : isFormPost(request) ? defaultNoJSHandler || (defaultNoJSHandler = createNoJSHandler()) : undefined;
|
|
1056
1146
|
const collectsFlight = !!(flightHook && instance && request.headers.has(SINGLE_FLIGHT_HEADER));
|
|
1057
|
-
|
|
1147
|
+
let parsed;
|
|
1148
|
+
try {
|
|
1149
|
+
parsed = await parseArguments(request, url, instance, codec);
|
|
1150
|
+
} catch {
|
|
1151
|
+
const response = new Response(DEV ? "Malformed server function arguments" : null, {
|
|
1152
|
+
status: 400
|
|
1153
|
+
});
|
|
1154
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1155
|
+
}
|
|
1058
1156
|
const flightContext = {
|
|
1059
1157
|
id: functionId,
|
|
1060
1158
|
args: parsed,
|
|
@@ -1201,7 +1299,34 @@ async function handleServerFunctionRequest(request, options = {}) {
|
|
|
1201
1299
|
}
|
|
1202
1300
|
};
|
|
1203
1301
|
const response = commitEventResponse(await dispatch(), event);
|
|
1204
|
-
return protectsRequest ? withCSRFVary(response) : response;
|
|
1302
|
+
return finalizeTransportResponse(protectsRequest ? withCSRFVary(response) : response, method);
|
|
1303
|
+
}
|
|
1304
|
+
function finalizeTransportResponse(response, method) {
|
|
1305
|
+
const stripBody = method === "HEAD" && response.body !== null;
|
|
1306
|
+
if (stripBody || !response.headers.has("Cache-Control")) {
|
|
1307
|
+
try {
|
|
1308
|
+
if (!response.headers.has("Cache-Control")) {
|
|
1309
|
+
response.headers.set("Cache-Control", "no-store");
|
|
1310
|
+
}
|
|
1311
|
+
if (!stripBody) return response;
|
|
1312
|
+
response.body.cancel().catch(() => {});
|
|
1313
|
+
return new Response(null, {
|
|
1314
|
+
status: response.status,
|
|
1315
|
+
statusText: response.statusText,
|
|
1316
|
+
headers: response.headers
|
|
1317
|
+
});
|
|
1318
|
+
} catch {
|
|
1319
|
+
const headers = new Headers(response.headers);
|
|
1320
|
+
if (!headers.has("Cache-Control")) headers.set("Cache-Control", "no-store");
|
|
1321
|
+
if (stripBody) response.body.cancel().catch(() => {});
|
|
1322
|
+
return new Response(stripBody ? null : response.body, {
|
|
1323
|
+
status: response.status,
|
|
1324
|
+
statusText: response.statusText,
|
|
1325
|
+
headers
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return response;
|
|
1205
1330
|
}
|
|
1206
1331
|
|
|
1207
|
-
export { ERROR_HEADER, FLASH_COOKIE,
|
|
1332
|
+
export { ERROR_HEADER, FLASH_COOKIE, GENERIC_SERVER_ERROR_MESSAGE, GET, INSTANCE_HEADER, SERVER_FUNCTION_INVOKE, SINGLE_FLIGHT_HEADER, clearFlashCookie, configureServerFunctionsServer, createNoJSHandler, createServerReference, decodeErrorHeaderValue, decodeFlashCookie, decodeResponse, decodeResponsePayload, encodeErrorHeaderValue, encodeFlashCookie, foldSetCookies, getEventServerFunctionInvocation, getServerFunction, getServerFunctionInvocation, getServerFunctionMetadata, handleServerFunctionRequest, hasFlashCookie, invoke, isServerFunction, live, observeServerFunctionCalls, parseServerFunctionUrl, registerServerFunction, registerServerReference, sanitizeServerError, serializeResponseStream, serverFunctionUrl, setServerFunctionsDev, subscribeFlightData, withMeta };
|
package/types/client.d.ts
CHANGED
|
@@ -79,6 +79,8 @@ export interface RequestEvent {
|
|
|
79
79
|
export type { CookieOptions } from "./cookies.js";
|
|
80
80
|
export type { ServerFunction, ServerFunctionMetadata, ServerFunctionRPC } from "./server-functions/shared.js";
|
|
81
81
|
export declare const waitAsset: (promise: Promise<unknown>) => void;
|
|
82
|
+
export declare let listDriver: ((parent: Node, listFn: any, marker?: Node, lateClassic?: () => void) => boolean) | undefined;
|
|
83
|
+
export declare function installListDriver(driver: typeof listDriver): void;
|
|
82
84
|
export { DOMWithState, ChildProperties, DOMElements, SVGElements, MathMLElements, VoidElements, RawTextElements, Namespaces, DelegatedEvents } from "./constants.js";
|
|
83
85
|
/** Client stub — hydration bootstrap is a server-only emit. */
|
|
84
86
|
export declare function generateHydrationScript(_options?: {
|
|
@@ -145,10 +147,10 @@ export declare function spread<T>(node: Element, accessor: T, skipChildren?: Boo
|
|
|
145
147
|
export declare function dynamicProperty(props: unknown, key: string): unknown;
|
|
146
148
|
export declare function applyRef<T extends Element = Element>(r: ((element: NoInfer<T>) => void) | ((element: NoInfer<T>) => void)[], element: T): void;
|
|
147
149
|
export declare function ref(fn: () => ((element: Element) => void) | ((element: Element) => void)[], element: Element): void;
|
|
148
|
-
|
|
150
|
+
/** Compiler-emitted primitive; not for hand-written code. @internal */
|
|
149
151
|
export declare function scope<T extends () => any>(fn: T): T;
|
|
150
152
|
export declare function installHydrationRuntime(): void;
|
|
151
|
-
|
|
153
|
+
/**
|
|
152
154
|
* Compiler-emitted primitive; not for hand-written code.
|
|
153
155
|
* @internal
|
|
154
156
|
*/
|
package/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Component } from "solid-js";
|
|
2
2
|
import type { JSX } from "./jsx.js";
|
|
3
3
|
export * from "./client.js";
|
|
4
|
+
export { patchDriver, rowProof, driveList } from "./patch-driver.js";
|
|
4
5
|
export * from "./server-mock.js";
|
|
5
6
|
export * from "./response.js";
|
|
6
7
|
export type { JSX } from "./jsx.js";
|
package/types/response.d.ts
CHANGED
|
@@ -16,7 +16,15 @@ export declare const ResponseEnvelope: {
|
|
|
16
16
|
export declare function isResponseEnvelope(value: unknown): value is ResponseEnvelope;
|
|
17
17
|
export declare const HREF: unique symbol;
|
|
18
18
|
export interface Href {
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The brand doubles as a channel: when the slot holds a string it is the
|
|
21
|
+
* value's *logical* path — the routable pathname before an integration's
|
|
22
|
+
* display rendering (eg. a hash router's `#` prefix). `redirect()` prefers
|
|
23
|
+
* it over coercion so Location headers carry routable paths; `toString()`
|
|
24
|
+
* remains the display href for the DOM. `true` brands a value whose
|
|
25
|
+
* string form is already logical.
|
|
26
|
+
*/
|
|
27
|
+
[HREF]: true | string;
|
|
20
28
|
toString(): string;
|
|
21
29
|
}
|
|
22
30
|
/** Whether `value` is an `Href`-branded URL-bearing value. */
|