@solidjs/start 1.2.1 → 1.3.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/config/index.d.ts +13 -0
- package/config/index.js +6 -0
- package/dist/runtime/serialization.d.ts +6 -0
- package/dist/runtime/serialization.js +212 -0
- package/dist/runtime/server-handler.js +30 -90
- package/dist/runtime/server-runtime.js +30 -116
- package/package.json +5 -5
package/config/index.d.ts
CHANGED
|
@@ -26,6 +26,19 @@ type SolidStartInlineConfig = {
|
|
|
26
26
|
experimental?: {
|
|
27
27
|
islands?: boolean;
|
|
28
28
|
};
|
|
29
|
+
serialization?: {
|
|
30
|
+
/**
|
|
31
|
+
* The serialization mode to use for server functions.
|
|
32
|
+
* The "js" mode uses a custom binary format that is more efficient than JSON, but requires a custom deserializer (with `eval()`) on the client.
|
|
33
|
+
* A strong CSP should block `eval()` executions, which would prevent the "js" mode from working.
|
|
34
|
+
* The "json" mode uses JSON for serialization, which is less efficient but can be deserialized with `JSON.parse` on the client.
|
|
35
|
+
*
|
|
36
|
+
* @default "js"
|
|
37
|
+
* @
|
|
38
|
+
* @warning on v2, "json" will be the default.
|
|
39
|
+
*/
|
|
40
|
+
mode?: "js" | "json";
|
|
41
|
+
};
|
|
29
42
|
vite?:
|
|
30
43
|
| ViteCustomizableConfig
|
|
31
44
|
| ((options: { router: "server" | "client" | "server-function" }) => ViteCustomizableConfig);
|
package/config/index.js
CHANGED
|
@@ -89,6 +89,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
});
|
|
92
|
+
|
|
92
93
|
const routeDir = join(start.appRoot, start.routeDir);
|
|
93
94
|
let server = start.server;
|
|
94
95
|
if (!start.ssr) {
|
|
@@ -99,6 +100,8 @@ export function defineConfig(baseConfig = {}) {
|
|
|
99
100
|
entryExtension = ".jsx";
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
const serializationMode = start.serialization?.mode || 'js'
|
|
104
|
+
|
|
102
105
|
return createApp({
|
|
103
106
|
server: {
|
|
104
107
|
compressPublicAssets: {
|
|
@@ -167,6 +170,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
167
170
|
"import.meta.env.SSR": JSON.stringify(true),
|
|
168
171
|
"import.meta.env.START_SSR": JSON.stringify(start.ssr),
|
|
169
172
|
"import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay),
|
|
173
|
+
"import.meta.env.SEROVAL_MODE": JSON.stringify(serializationMode),
|
|
170
174
|
...userConfig.define
|
|
171
175
|
}
|
|
172
176
|
})
|
|
@@ -234,6 +238,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
234
238
|
"import.meta.env.START_SSR": JSON.stringify(start.ssr),
|
|
235
239
|
"import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay),
|
|
236
240
|
"import.meta.env.SERVER_BASE_URL": JSON.stringify(server?.baseURL ?? ""),
|
|
241
|
+
"import.meta.env.SEROVAL_MODE": JSON.stringify(serializationMode),
|
|
237
242
|
...userConfig.define
|
|
238
243
|
}
|
|
239
244
|
})
|
|
@@ -293,6 +298,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
293
298
|
"import.meta.env.SSR": JSON.stringify(true),
|
|
294
299
|
"import.meta.env.START_SSR": JSON.stringify(start.ssr),
|
|
295
300
|
"import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay),
|
|
301
|
+
"import.meta.env.SEROVAL_MODE": JSON.stringify(serializationMode),
|
|
296
302
|
...userConfig.define
|
|
297
303
|
}
|
|
298
304
|
})
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function serializeToJSStream(id: string, value: any): ReadableStream<any>;
|
|
2
|
+
export declare function serializeToJSONStream(value: any): ReadableStream<any>;
|
|
3
|
+
export declare function serializeToJSONString(value: any): Promise<string>;
|
|
4
|
+
export declare function deserializeFromJSONString(json: string): Promise<unknown>;
|
|
5
|
+
export declare function deserializeJSONStream(response: Response | Request): Promise<unknown>;
|
|
6
|
+
export declare function deserializeJSStream(id: string, response: Response): Promise<unknown>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { crossSerializeStream, deserialize, Feature, fromCrossJSON, fromJSON, getCrossReferenceHeader, toCrossJSONStream, toJSONAsync, } from "seroval";
|
|
2
|
+
import { AbortSignalPlugin, CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLPlugin, URLSearchParamsPlugin, } from "seroval-plugins/web";
|
|
3
|
+
// TODO(Alexis): if we can, allow providing an option to extend these.
|
|
4
|
+
const DEFAULT_PLUGINS = [
|
|
5
|
+
AbortSignalPlugin,
|
|
6
|
+
CustomEventPlugin,
|
|
7
|
+
DOMExceptionPlugin,
|
|
8
|
+
EventPlugin,
|
|
9
|
+
FormDataPlugin,
|
|
10
|
+
HeadersPlugin,
|
|
11
|
+
ReadableStreamPlugin,
|
|
12
|
+
RequestPlugin,
|
|
13
|
+
ResponsePlugin,
|
|
14
|
+
URLSearchParamsPlugin,
|
|
15
|
+
URLPlugin,
|
|
16
|
+
];
|
|
17
|
+
const MAX_SERIALIZATION_DEPTH_LIMIT = 64;
|
|
18
|
+
const DISABLED_FEATURES = Feature.RegExp;
|
|
19
|
+
/**
|
|
20
|
+
* Alexis:
|
|
21
|
+
*
|
|
22
|
+
* A "chunk" is a piece of data emitted by the streaming serializer.
|
|
23
|
+
* Each chunk is represented by a 32-bit value (encoded in hexadecimal),
|
|
24
|
+
* followed by the encoded string (8-bit representation). This format
|
|
25
|
+
* is important so we know how much of the chunk being streamed we
|
|
26
|
+
* are expecting before parsing the entire string data.
|
|
27
|
+
*
|
|
28
|
+
* This is sort of a bootleg "multipart/form-data" except it's bad at
|
|
29
|
+
* handling File/Blob LOL
|
|
30
|
+
*
|
|
31
|
+
* The format is as follows:
|
|
32
|
+
* ;0xFFFFFFFF;<string data>
|
|
33
|
+
*/
|
|
34
|
+
function createChunk(data) {
|
|
35
|
+
const encodeData = new TextEncoder().encode(data);
|
|
36
|
+
const bytes = encodeData.length;
|
|
37
|
+
const baseHex = bytes.toString(16);
|
|
38
|
+
const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit
|
|
39
|
+
const head = new TextEncoder().encode(`;0x${totalHex};`);
|
|
40
|
+
const chunk = new Uint8Array(12 + bytes);
|
|
41
|
+
chunk.set(head);
|
|
42
|
+
chunk.set(encodeData, 12);
|
|
43
|
+
return chunk;
|
|
44
|
+
}
|
|
45
|
+
export function serializeToJSStream(id, value) {
|
|
46
|
+
return new ReadableStream({
|
|
47
|
+
start(controller) {
|
|
48
|
+
crossSerializeStream(value, {
|
|
49
|
+
scopeId: id,
|
|
50
|
+
plugins: DEFAULT_PLUGINS,
|
|
51
|
+
onSerialize(data, initial) {
|
|
52
|
+
controller.enqueue(createChunk(initial ? `(${getCrossReferenceHeader(id)},${data})` : data));
|
|
53
|
+
},
|
|
54
|
+
onDone() {
|
|
55
|
+
controller.close();
|
|
56
|
+
},
|
|
57
|
+
onError(error) {
|
|
58
|
+
controller.error(error);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function serializeToJSONStream(value) {
|
|
65
|
+
return new ReadableStream({
|
|
66
|
+
start(controller) {
|
|
67
|
+
toCrossJSONStream(value, {
|
|
68
|
+
disabledFeatures: DISABLED_FEATURES,
|
|
69
|
+
depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT,
|
|
70
|
+
plugins: DEFAULT_PLUGINS,
|
|
71
|
+
onParse(node) {
|
|
72
|
+
controller.enqueue(createChunk(JSON.stringify(node)));
|
|
73
|
+
},
|
|
74
|
+
onDone() {
|
|
75
|
+
controller.close();
|
|
76
|
+
},
|
|
77
|
+
onError(error) {
|
|
78
|
+
controller.error(error);
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
class SerovalChunkReader {
|
|
85
|
+
reader;
|
|
86
|
+
buffer;
|
|
87
|
+
done;
|
|
88
|
+
constructor(stream) {
|
|
89
|
+
this.reader = stream.getReader();
|
|
90
|
+
this.buffer = new Uint8Array(0);
|
|
91
|
+
this.done = false;
|
|
92
|
+
}
|
|
93
|
+
async readChunk() {
|
|
94
|
+
// if there's no chunk, read again
|
|
95
|
+
const chunk = await this.reader.read();
|
|
96
|
+
if (!chunk.done) {
|
|
97
|
+
// repopulate the buffer
|
|
98
|
+
const newBuffer = new Uint8Array(this.buffer.length + chunk.value.length);
|
|
99
|
+
newBuffer.set(this.buffer);
|
|
100
|
+
newBuffer.set(chunk.value, this.buffer.length);
|
|
101
|
+
this.buffer = newBuffer;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.done = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async next() {
|
|
108
|
+
// Check if the buffer is empty
|
|
109
|
+
if (this.buffer.length === 0) {
|
|
110
|
+
// if we are already done...
|
|
111
|
+
if (this.done) {
|
|
112
|
+
return {
|
|
113
|
+
done: true,
|
|
114
|
+
value: undefined,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// Otherwise, read a new chunk
|
|
118
|
+
await this.readChunk();
|
|
119
|
+
return await this.next();
|
|
120
|
+
}
|
|
121
|
+
// Read the "byte header"
|
|
122
|
+
// The byte header tells us how big the expected data is
|
|
123
|
+
// so we know how much data we should wait before we
|
|
124
|
+
// deserialize the data
|
|
125
|
+
const head = new TextDecoder().decode(this.buffer.subarray(1, 11));
|
|
126
|
+
const bytes = Number.parseInt(head, 16); // ;0x00000000;
|
|
127
|
+
// Check if the buffer has enough bytes to be parsed
|
|
128
|
+
while (bytes > this.buffer.length - 12) {
|
|
129
|
+
// If it's not enough, and the reader is done
|
|
130
|
+
// then the chunk is invalid.
|
|
131
|
+
if (this.done) {
|
|
132
|
+
throw new Error("Malformed server function stream.");
|
|
133
|
+
}
|
|
134
|
+
// Otherwise, we read more chunks
|
|
135
|
+
await this.readChunk();
|
|
136
|
+
}
|
|
137
|
+
// Extract the exact chunk as defined by the byte header
|
|
138
|
+
const partial = new TextDecoder().decode(this.buffer.subarray(12, 12 + bytes));
|
|
139
|
+
// The rest goes to the buffer
|
|
140
|
+
this.buffer = this.buffer.subarray(12 + bytes);
|
|
141
|
+
// Deserialize the chunk
|
|
142
|
+
return {
|
|
143
|
+
done: false,
|
|
144
|
+
value: partial,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async drain(interpret) {
|
|
148
|
+
while (true) {
|
|
149
|
+
const result = await this.next();
|
|
150
|
+
if (result.done) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
interpret(result.value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export async function serializeToJSONString(value) {
|
|
160
|
+
// const response = new Response(serializeToJSONStream(value));
|
|
161
|
+
// return await response.text();
|
|
162
|
+
return JSON.stringify(await toJSONAsync(value, {
|
|
163
|
+
plugins: DEFAULT_PLUGINS,
|
|
164
|
+
depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT,
|
|
165
|
+
disabledFeatures: DISABLED_FEATURES,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
export async function deserializeFromJSONString(json) {
|
|
169
|
+
return fromJSON(JSON.parse(json), {
|
|
170
|
+
plugins: DEFAULT_PLUGINS,
|
|
171
|
+
disabledFeatures: DISABLED_FEATURES,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export async function deserializeJSONStream(response) {
|
|
175
|
+
if (!response.body) {
|
|
176
|
+
throw new Error("missing body");
|
|
177
|
+
}
|
|
178
|
+
const reader = new SerovalChunkReader(response.body);
|
|
179
|
+
const result = await reader.next();
|
|
180
|
+
if (!result.done) {
|
|
181
|
+
const refs = new Map();
|
|
182
|
+
function interpretChunk(chunk) {
|
|
183
|
+
const value = fromCrossJSON(JSON.parse(chunk), {
|
|
184
|
+
refs,
|
|
185
|
+
disabledFeatures: DISABLED_FEATURES,
|
|
186
|
+
depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT,
|
|
187
|
+
plugins: DEFAULT_PLUGINS,
|
|
188
|
+
});
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
void reader.drain(interpretChunk);
|
|
192
|
+
return interpretChunk(result.value);
|
|
193
|
+
}
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
export async function deserializeJSStream(id, response) {
|
|
197
|
+
if (!response.body) {
|
|
198
|
+
throw new Error("missing body");
|
|
199
|
+
}
|
|
200
|
+
const reader = new SerovalChunkReader(response.body);
|
|
201
|
+
const result = await reader.next();
|
|
202
|
+
if (!result.done) {
|
|
203
|
+
reader.drain(deserialize).then(() => {
|
|
204
|
+
// @ts-ignore
|
|
205
|
+
delete $R[id];
|
|
206
|
+
}, () => {
|
|
207
|
+
// no-op
|
|
208
|
+
});
|
|
209
|
+
return deserialize(result.value);
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/// <reference types="vinxi/types/server" />
|
|
2
2
|
import { parseSetCookie } from "cookie-es";
|
|
3
|
-
import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval";
|
|
4
|
-
// @ts-ignore
|
|
5
|
-
import { CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLPlugin, URLSearchParamsPlugin } from "seroval-plugins/web";
|
|
6
3
|
import { sharedConfig } from "solid-js";
|
|
7
4
|
import { renderToString } from "solid-js/web";
|
|
8
5
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
@@ -13,47 +10,7 @@ import { getExpectedRedirectStatus } from "../server/handler";
|
|
|
13
10
|
import { createPageEvent } from "../server/pageEvent";
|
|
14
11
|
// @ts-ignore
|
|
15
12
|
import serverFnManifest from "solidstart:server-fn-manifest";
|
|
16
|
-
|
|
17
|
-
const encodeData = new TextEncoder().encode(data);
|
|
18
|
-
const bytes = encodeData.length;
|
|
19
|
-
const baseHex = bytes.toString(16);
|
|
20
|
-
const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit
|
|
21
|
-
const head = new TextEncoder().encode(`;0x${totalHex};`);
|
|
22
|
-
const chunk = new Uint8Array(12 + bytes);
|
|
23
|
-
chunk.set(head);
|
|
24
|
-
chunk.set(encodeData, 12);
|
|
25
|
-
return chunk;
|
|
26
|
-
}
|
|
27
|
-
function serializeToStream(id, value) {
|
|
28
|
-
return new ReadableStream({
|
|
29
|
-
start(controller) {
|
|
30
|
-
crossSerializeStream(value, {
|
|
31
|
-
scopeId: id,
|
|
32
|
-
plugins: [
|
|
33
|
-
CustomEventPlugin,
|
|
34
|
-
DOMExceptionPlugin,
|
|
35
|
-
EventPlugin,
|
|
36
|
-
FormDataPlugin,
|
|
37
|
-
HeadersPlugin,
|
|
38
|
-
ReadableStreamPlugin,
|
|
39
|
-
RequestPlugin,
|
|
40
|
-
ResponsePlugin,
|
|
41
|
-
URLSearchParamsPlugin,
|
|
42
|
-
URLPlugin
|
|
43
|
-
],
|
|
44
|
-
onSerialize(data, initial) {
|
|
45
|
-
controller.enqueue(createChunk(initial ? `(${getCrossReferenceHeader(id)},${data})` : data));
|
|
46
|
-
},
|
|
47
|
-
onDone() {
|
|
48
|
-
controller.close();
|
|
49
|
-
},
|
|
50
|
-
onError(error) {
|
|
51
|
-
controller.error(error);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
13
|
+
import { deserializeFromJSONString, serializeToJSONStream, serializeToJSStream } from "./serialization";
|
|
57
14
|
async function handleServerFunction(h3Event) {
|
|
58
15
|
const event = getFetchEvent(h3Event);
|
|
59
16
|
const request = event.request;
|
|
@@ -64,7 +21,7 @@ async function handleServerFunction(h3Event) {
|
|
|
64
21
|
let functionId, name;
|
|
65
22
|
if (serverReference) {
|
|
66
23
|
invariant(typeof serverReference === "string", "Invalid server function");
|
|
67
|
-
[functionId, name] = serverReference.split("#");
|
|
24
|
+
[functionId, name] = decodeURIComponent(serverReference).split("#");
|
|
68
25
|
}
|
|
69
26
|
else {
|
|
70
27
|
functionId = url.searchParams.get("id");
|
|
@@ -100,23 +57,10 @@ async function handleServerFunction(h3Event) {
|
|
|
100
57
|
if (!instance || h3Event.method === "GET") {
|
|
101
58
|
const args = url.searchParams.get("args");
|
|
102
59
|
if (args) {
|
|
103
|
-
const
|
|
104
|
-
(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
CustomEventPlugin,
|
|
108
|
-
DOMExceptionPlugin,
|
|
109
|
-
EventPlugin,
|
|
110
|
-
FormDataPlugin,
|
|
111
|
-
HeadersPlugin,
|
|
112
|
-
ReadableStreamPlugin,
|
|
113
|
-
RequestPlugin,
|
|
114
|
-
ResponsePlugin,
|
|
115
|
-
URLSearchParamsPlugin,
|
|
116
|
-
URLPlugin
|
|
117
|
-
]
|
|
118
|
-
})
|
|
119
|
-
: json).forEach((arg) => parsed.push(arg));
|
|
60
|
+
const result = (await deserializeFromJSONString(args));
|
|
61
|
+
for (const arg of result) {
|
|
62
|
+
parsed.push(arg);
|
|
63
|
+
}
|
|
120
64
|
}
|
|
121
65
|
}
|
|
122
66
|
if (h3Event.method === "POST") {
|
|
@@ -129,38 +73,26 @@ async function handleServerFunction(h3Event) {
|
|
|
129
73
|
const isH3EventBodyStreamLocked = (isReadableStream && h3Request.locked) ||
|
|
130
74
|
(hasReadableStream && h3Request.body.locked);
|
|
131
75
|
const requestBody = isReadableStream ? h3Request : h3Request.body;
|
|
132
|
-
|
|
76
|
+
// workaround for https://github.com/unjs/nitro/issues/1721
|
|
77
|
+
// (issue only in edge runtimes and netlify preset)
|
|
78
|
+
const tmpReq = isH3EventBodyStreamLocked
|
|
79
|
+
? request
|
|
80
|
+
: new Request(request, { ...request, body: requestBody });
|
|
81
|
+
if (request.headers.get('x-serialized')) {
|
|
82
|
+
parsed = await deserializeFromJSONString(await tmpReq.text());
|
|
83
|
+
}
|
|
84
|
+
else if (contentType?.startsWith("multipart/form-data") ||
|
|
133
85
|
contentType?.startsWith("application/x-www-form-urlencoded")) {
|
|
134
86
|
// workaround for https://github.com/unjs/nitro/issues/1721
|
|
135
87
|
// (issue only in edge runtimes and netlify preset)
|
|
136
|
-
parsed.push(await (
|
|
137
|
-
? request
|
|
138
|
-
: new Request(request, { ...request, body: requestBody })).formData());
|
|
88
|
+
parsed.push(await tmpReq.formData());
|
|
139
89
|
// what should work when #1721 is fixed
|
|
140
90
|
// parsed.push(await request.formData);
|
|
141
91
|
}
|
|
142
|
-
else if (contentType?.startsWith(
|
|
143
|
-
// workaround for https://github.com/unjs/nitro/issues/1721
|
|
144
|
-
// (issue only in edge runtimes and netlify preset)
|
|
145
|
-
const tmpReq = isH3EventBodyStreamLocked
|
|
146
|
-
? request
|
|
147
|
-
: new Request(request, { ...request, body: requestBody });
|
|
92
|
+
else if (contentType?.startsWith('application/json')) {
|
|
148
93
|
// what should work when #1721 is fixed
|
|
149
94
|
// just use request.json() here
|
|
150
|
-
parsed =
|
|
151
|
-
plugins: [
|
|
152
|
-
CustomEventPlugin,
|
|
153
|
-
DOMExceptionPlugin,
|
|
154
|
-
EventPlugin,
|
|
155
|
-
FormDataPlugin,
|
|
156
|
-
HeadersPlugin,
|
|
157
|
-
ReadableStreamPlugin,
|
|
158
|
-
RequestPlugin,
|
|
159
|
-
ResponsePlugin,
|
|
160
|
-
URLSearchParamsPlugin,
|
|
161
|
-
URLPlugin
|
|
162
|
-
]
|
|
163
|
-
});
|
|
95
|
+
parsed = await tmpReq.json();
|
|
164
96
|
}
|
|
165
97
|
}
|
|
166
98
|
try {
|
|
@@ -196,8 +128,12 @@ async function handleServerFunction(h3Event) {
|
|
|
196
128
|
// handle no JS success case
|
|
197
129
|
if (!instance)
|
|
198
130
|
return handleNoJS(result, request, parsed);
|
|
199
|
-
setHeader(h3Event,
|
|
200
|
-
|
|
131
|
+
setHeader(h3Event, 'x-serialized', 'true');
|
|
132
|
+
if (import.meta.env.SEROVAL_MODE === 'js') {
|
|
133
|
+
setHeader(h3Event, "content-type", "text/javascript");
|
|
134
|
+
return serializeToJSStream(instance, result);
|
|
135
|
+
}
|
|
136
|
+
return serializeToJSONStream(result);
|
|
201
137
|
}
|
|
202
138
|
catch (x) {
|
|
203
139
|
if (x instanceof Response) {
|
|
@@ -225,8 +161,12 @@ async function handleServerFunction(h3Event) {
|
|
|
225
161
|
x = handleNoJS(x, request, parsed, true);
|
|
226
162
|
}
|
|
227
163
|
if (instance) {
|
|
228
|
-
setHeader(h3Event,
|
|
229
|
-
|
|
164
|
+
setHeader(h3Event, 'x-serialized', 'true');
|
|
165
|
+
if (import.meta.env.SEROVAL_MODE === 'js') {
|
|
166
|
+
setHeader(h3Event, "content-type", "text/javascript");
|
|
167
|
+
return serializeToJSStream(instance, x);
|
|
168
|
+
}
|
|
169
|
+
return serializeToJSONStream(x);
|
|
230
170
|
}
|
|
231
171
|
return x;
|
|
232
172
|
}
|
|
@@ -1,97 +1,5 @@
|
|
|
1
|
-
import { deserialize, toJSONAsync } from "seroval";
|
|
2
|
-
import { CustomEventPlugin, DOMExceptionPlugin, EventPlugin, FormDataPlugin, HeadersPlugin, ReadableStreamPlugin, RequestPlugin, ResponsePlugin, URLPlugin, URLSearchParamsPlugin } from "seroval-plugins/web";
|
|
3
1
|
import { createIslandReference } from "../server/islands/index";
|
|
4
|
-
|
|
5
|
-
reader;
|
|
6
|
-
buffer;
|
|
7
|
-
done;
|
|
8
|
-
constructor(stream) {
|
|
9
|
-
this.reader = stream.getReader();
|
|
10
|
-
this.buffer = new Uint8Array(0);
|
|
11
|
-
this.done = false;
|
|
12
|
-
}
|
|
13
|
-
async readChunk() {
|
|
14
|
-
// if there's no chunk, read again
|
|
15
|
-
const chunk = await this.reader.read();
|
|
16
|
-
if (!chunk.done) {
|
|
17
|
-
// repopulate the buffer
|
|
18
|
-
let newBuffer = new Uint8Array(this.buffer.length + chunk.value.length);
|
|
19
|
-
newBuffer.set(this.buffer);
|
|
20
|
-
newBuffer.set(chunk.value, this.buffer.length);
|
|
21
|
-
this.buffer = newBuffer;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
this.done = true;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
async next() {
|
|
28
|
-
// Check if the buffer is empty
|
|
29
|
-
if (this.buffer.length === 0) {
|
|
30
|
-
// if we are already done...
|
|
31
|
-
if (this.done) {
|
|
32
|
-
return {
|
|
33
|
-
done: true,
|
|
34
|
-
value: undefined
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
// Otherwise, read a new chunk
|
|
38
|
-
await this.readChunk();
|
|
39
|
-
return await this.next();
|
|
40
|
-
}
|
|
41
|
-
// Read the "byte header"
|
|
42
|
-
// The byte header tells us how big the expected data is
|
|
43
|
-
// so we know how much data we should wait before we
|
|
44
|
-
// deserialize the data
|
|
45
|
-
const head = new TextDecoder().decode(this.buffer.subarray(1, 11));
|
|
46
|
-
const bytes = Number.parseInt(head, 16); // ;0x00000000;
|
|
47
|
-
if (Number.isNaN(bytes)) {
|
|
48
|
-
throw new Error(`Malformed server function stream header: ${head}`);
|
|
49
|
-
}
|
|
50
|
-
// Check if the buffer has enough bytes to be parsed
|
|
51
|
-
while (bytes > this.buffer.length - 12) {
|
|
52
|
-
// If it's not enough, and the reader is done
|
|
53
|
-
// then the chunk is invalid.
|
|
54
|
-
if (this.done) {
|
|
55
|
-
throw new Error("Malformed server function stream.");
|
|
56
|
-
}
|
|
57
|
-
// Otherwise, we read more chunks
|
|
58
|
-
await this.readChunk();
|
|
59
|
-
}
|
|
60
|
-
// Extract the exact chunk as defined by the byte header
|
|
61
|
-
const partial = new TextDecoder().decode(this.buffer.subarray(12, 12 + bytes));
|
|
62
|
-
// The rest goes to the buffer
|
|
63
|
-
this.buffer = this.buffer.subarray(12 + bytes);
|
|
64
|
-
// Deserialize the chunk
|
|
65
|
-
return {
|
|
66
|
-
done: false,
|
|
67
|
-
value: deserialize(partial)
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
async drain() {
|
|
71
|
-
while (true) {
|
|
72
|
-
const result = await this.next();
|
|
73
|
-
if (result.done) {
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
async function deserializeStream(id, response) {
|
|
80
|
-
if (!response.body) {
|
|
81
|
-
throw new Error("missing body");
|
|
82
|
-
}
|
|
83
|
-
const reader = new SerovalChunkReader(response.body);
|
|
84
|
-
const result = await reader.next();
|
|
85
|
-
if (!result.done) {
|
|
86
|
-
reader.drain().then(() => {
|
|
87
|
-
// @ts-ignore
|
|
88
|
-
delete $R[id];
|
|
89
|
-
}, () => {
|
|
90
|
-
// no-op
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
return result.value;
|
|
94
|
-
}
|
|
2
|
+
import { deserializeJSONStream, deserializeJSStream, serializeToJSONString, } from "./serialization";
|
|
95
3
|
let INSTANCE = 0;
|
|
96
4
|
function createRequest(base, id, instance, options) {
|
|
97
5
|
return fetch(base, {
|
|
@@ -99,23 +7,11 @@ function createRequest(base, id, instance, options) {
|
|
|
99
7
|
...options,
|
|
100
8
|
headers: {
|
|
101
9
|
...options.headers,
|
|
102
|
-
"X-Server-Id": id,
|
|
10
|
+
"X-Server-Id": encodeURIComponent(id),
|
|
103
11
|
"X-Server-Instance": instance
|
|
104
12
|
}
|
|
105
13
|
});
|
|
106
14
|
}
|
|
107
|
-
const plugins = [
|
|
108
|
-
CustomEventPlugin,
|
|
109
|
-
DOMExceptionPlugin,
|
|
110
|
-
EventPlugin,
|
|
111
|
-
FormDataPlugin,
|
|
112
|
-
HeadersPlugin,
|
|
113
|
-
ReadableStreamPlugin,
|
|
114
|
-
RequestPlugin,
|
|
115
|
-
ResponsePlugin,
|
|
116
|
-
URLSearchParamsPlugin,
|
|
117
|
-
URLPlugin
|
|
118
|
-
];
|
|
119
15
|
async function fetchServerFunction(base, id, options, args) {
|
|
120
16
|
const instance = `server-fn:${INSTANCE++}`;
|
|
121
17
|
const response = await (args.length === 0
|
|
@@ -126,12 +22,21 @@ async function fetchServerFunction(base, id, options, args) {
|
|
|
126
22
|
? createRequest(base, id, instance, {
|
|
127
23
|
...options,
|
|
128
24
|
body: args[0],
|
|
129
|
-
headers: {
|
|
25
|
+
headers: {
|
|
26
|
+
...options.headers,
|
|
27
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
28
|
+
},
|
|
130
29
|
})
|
|
131
30
|
: createRequest(base, id, instance, {
|
|
132
31
|
...options,
|
|
133
|
-
body:
|
|
134
|
-
|
|
32
|
+
body: await serializeToJSONString(args),
|
|
33
|
+
// duplex: 'half',
|
|
34
|
+
// body: serializeToJSONStream(args),
|
|
35
|
+
headers: {
|
|
36
|
+
...options.headers,
|
|
37
|
+
"x-serialized": "true",
|
|
38
|
+
"Content-Type": "text/plain"
|
|
39
|
+
},
|
|
135
40
|
}));
|
|
136
41
|
if (response.headers.has("Location") ||
|
|
137
42
|
response.headers.has("X-Revalidate") ||
|
|
@@ -139,21 +44,30 @@ async function fetchServerFunction(base, id, options, args) {
|
|
|
139
44
|
if (response.body) {
|
|
140
45
|
/* @ts-ignore-next-line */
|
|
141
46
|
response.customBody = () => {
|
|
142
|
-
|
|
47
|
+
if (import.meta.env.SEROVAL_MODE === "js") {
|
|
48
|
+
return deserializeJSStream(instance, response.clone());
|
|
49
|
+
}
|
|
50
|
+
return deserializeJSONStream(response.clone());
|
|
143
51
|
};
|
|
144
52
|
}
|
|
145
53
|
return response;
|
|
146
54
|
}
|
|
147
55
|
const contentType = response.headers.get("Content-Type");
|
|
56
|
+
const cloned = response.clone();
|
|
148
57
|
let result;
|
|
149
58
|
if (contentType && contentType.startsWith("text/plain")) {
|
|
150
|
-
result = await
|
|
59
|
+
result = await cloned.text();
|
|
151
60
|
}
|
|
152
61
|
else if (contentType && contentType.startsWith("application/json")) {
|
|
153
|
-
result = await
|
|
62
|
+
result = await cloned.json();
|
|
154
63
|
}
|
|
155
|
-
else {
|
|
156
|
-
|
|
64
|
+
else if (response.headers.get("x-serialized")) {
|
|
65
|
+
if (import.meta.env.SEROVAL_MODE === "js") {
|
|
66
|
+
result = await deserializeJSStream(instance, cloned);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result = await deserializeJSONStream(cloned);
|
|
70
|
+
}
|
|
157
71
|
}
|
|
158
72
|
if (response.headers.has("X-Error")) {
|
|
159
73
|
throw result;
|
|
@@ -178,7 +92,7 @@ export function createServerReference(fn, id, name) {
|
|
|
178
92
|
return fetchServerFunction(encodeArgs
|
|
179
93
|
? url +
|
|
180
94
|
(args.length
|
|
181
|
-
? `&args=${encodeURIComponent(
|
|
95
|
+
? `&args=${encodeURIComponent(await serializeToJSONString(args))}`
|
|
182
96
|
: "")
|
|
183
97
|
: `${baseURL}/_server`, `${id}#${name}`, options, encodeArgs ? [] : args);
|
|
184
98
|
};
|
|
@@ -190,7 +104,7 @@ export function createServerReference(fn, id, name) {
|
|
|
190
104
|
},
|
|
191
105
|
apply(target, thisArg, args) {
|
|
192
106
|
return fetchServerFunction(`${baseURL}/_server`, `${id}#${name}`, {}, args);
|
|
193
|
-
}
|
|
107
|
+
},
|
|
194
108
|
});
|
|
195
109
|
}
|
|
196
110
|
export function createClientReference(Component, id, name) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidjs/start",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Ryan Carniato",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"solid-js": "^1.9.
|
|
58
|
+
"solid-js": "^1.9.11",
|
|
59
59
|
"vinxi": "^0.5.7",
|
|
60
60
|
"vitest": "3.0.5"
|
|
61
61
|
},
|
|
@@ -68,13 +68,13 @@
|
|
|
68
68
|
"error-stack-parser": "^2.1.4",
|
|
69
69
|
"html-to-image": "^1.11.11",
|
|
70
70
|
"radix3": "^1.1.0",
|
|
71
|
-
"seroval": "^1.
|
|
72
|
-
"seroval-plugins": "^1.
|
|
71
|
+
"seroval": "^1.5.0",
|
|
72
|
+
"seroval-plugins": "^1.5.0",
|
|
73
73
|
"shiki": "^1.26.1",
|
|
74
74
|
"source-map-js": "^1.0.2",
|
|
75
75
|
"terracotta": "^1.0.4",
|
|
76
76
|
"tinyglobby": "^0.2.2",
|
|
77
|
-
"vite-plugin-solid": "^2.11.
|
|
77
|
+
"vite-plugin-solid": "^2.11.10"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
80
|
"vinxi": "^0.5.7"
|