@solidjs/start 0.4.7 → 0.4.9
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/server-runtime.jsx +84 -38
- package/entry/index.ts +106 -0
- package/entry/spa/index.ts +24 -0
- package/package.json +4 -3
- package/shared/routes.ts +1 -1
|
@@ -13,54 +13,100 @@ import {
|
|
|
13
13
|
} from 'seroval-plugins/web';
|
|
14
14
|
import { createIslandReference } from "../server/islands";
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
class ChunkReader {
|
|
17
|
+
constructor(stream) {
|
|
18
|
+
this.reader = stream.getReader();
|
|
19
|
+
this.buffer = '';
|
|
20
|
+
this.done = false;
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
deserialize(split);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
await pop();
|
|
22
|
+
|
|
23
|
+
async readChunk() {
|
|
24
|
+
// if there's no chunk, read again
|
|
25
|
+
const chunk = await this.reader.read();
|
|
26
|
+
if (!chunk.done) {
|
|
27
|
+
// repopulate the buffer
|
|
28
|
+
this.buffer += new TextDecoder().decode(chunk.value);
|
|
29
|
+
} else {
|
|
30
|
+
this.done = true;
|
|
33
31
|
}
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
async nextValue() {
|
|
35
|
+
// Check if the buffer is empty
|
|
36
|
+
if (this.buffer === '') {
|
|
37
|
+
// if we are already one...
|
|
38
|
+
if (this.done) {
|
|
39
|
+
return {
|
|
40
|
+
done: true,
|
|
41
|
+
value: undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Otherwise, read a new chunk
|
|
45
|
+
await this.readChunk();
|
|
46
|
+
}
|
|
47
|
+
// Get the first valid seroval chunk
|
|
48
|
+
const [first, ...rest] = this.buffer.split('\n');
|
|
49
|
+
// Deserialize the seroval chunk
|
|
50
|
+
const result = {
|
|
51
|
+
done: false,
|
|
52
|
+
value: deserialize(first),
|
|
53
|
+
};
|
|
54
|
+
// if it succeeds, remove the first valid chunk
|
|
55
|
+
// from the buffer
|
|
56
|
+
this.buffer = rest.join('\n');
|
|
57
|
+
return result;
|
|
39
58
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
|
|
60
|
+
async next() {
|
|
61
|
+
try {
|
|
62
|
+
// Attempt to read a valid seroval chunk
|
|
63
|
+
return await this.nextValue();
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// If it happens that there's an error again
|
|
66
|
+
// and we are done reading the buffer
|
|
67
|
+
// then the whole stream is invalid.
|
|
68
|
+
if (this.done) {
|
|
69
|
+
throw new Error('Malformed server function stream.');
|
|
50
70
|
}
|
|
71
|
+
// Since it's invalid (some syntax-related issue)
|
|
72
|
+
// we read a new chunk, and hope there's a valid
|
|
73
|
+
// seroval chunk there
|
|
74
|
+
await this.readChunk();
|
|
75
|
+
// Retry again
|
|
76
|
+
return await this.next();
|
|
51
77
|
}
|
|
52
78
|
}
|
|
53
79
|
|
|
54
|
-
|
|
55
|
-
()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
async drain() {
|
|
81
|
+
while (true) {
|
|
82
|
+
const result = await this.next();
|
|
83
|
+
if (result.done) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
60
86
|
}
|
|
61
|
-
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function deserializeStream(id, response) {
|
|
91
|
+
if (!response.body) {
|
|
92
|
+
throw new Error("missing body");
|
|
93
|
+
}
|
|
94
|
+
const reader = new ChunkReader(response.body);
|
|
95
|
+
|
|
96
|
+
const result = await reader.next();
|
|
97
|
+
|
|
98
|
+
if (!result.done) {
|
|
99
|
+
reader.drain().then(
|
|
100
|
+
() => {
|
|
101
|
+
delete $R[id];
|
|
102
|
+
},
|
|
103
|
+
() => {
|
|
104
|
+
// no-op
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
62
108
|
|
|
63
|
-
return
|
|
109
|
+
return result.value;
|
|
64
110
|
}
|
|
65
111
|
|
|
66
112
|
let INSTANCE = 0;
|
|
@@ -108,7 +154,7 @@ async function fetchServerFunction(base, id, args) {
|
|
|
108
154
|
let result;
|
|
109
155
|
if (contentType && contentType.startsWith("text/plain")) {
|
|
110
156
|
result = await response.text();
|
|
111
|
-
} else if(contentType && contentType.startsWith("application/json")) {
|
|
157
|
+
} else if (contentType && contentType.startsWith("application/json")) {
|
|
112
158
|
result = await response.json();
|
|
113
159
|
} else {
|
|
114
160
|
result = deserializeStream(instance, response);
|
package/entry/index.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { sharedConfig } from "solid-js";
|
|
2
|
+
import { renderToStream } from "solid-js/web";
|
|
3
|
+
/* @ts-ignore */
|
|
4
|
+
import { provideRequestEvent } from "solid-js/web/storage";
|
|
5
|
+
import {
|
|
6
|
+
EventHandlerObject,
|
|
7
|
+
EventHandlerRequest,
|
|
8
|
+
H3Event,
|
|
9
|
+
HTTPMethod,
|
|
10
|
+
eventHandler,
|
|
11
|
+
sendRedirect,
|
|
12
|
+
setHeader,
|
|
13
|
+
setResponseStatus
|
|
14
|
+
} from "vinxi/server";
|
|
15
|
+
import { getFetchEvent } from "../server/middleware";
|
|
16
|
+
import { createPageEvent } from "../server/page-event";
|
|
17
|
+
import { APIEvent, FetchEvent, PageEvent } from "../server/types";
|
|
18
|
+
import { matchAPIRoute } from "../shared/routes";
|
|
19
|
+
|
|
20
|
+
export function createBaseHandler(
|
|
21
|
+
fn: (context: PageEvent) => unknown,
|
|
22
|
+
options: {
|
|
23
|
+
nonce?: string;
|
|
24
|
+
renderId?: string;
|
|
25
|
+
timeoutMs?: number;
|
|
26
|
+
onCompleteAll?: (options: { write: (v: any) => void }) => void;
|
|
27
|
+
onCompleteShell?: (options: { write: (v: any) => void }) => void;
|
|
28
|
+
createPageEvent?: (event: FetchEvent) => Promise<PageEvent>;
|
|
29
|
+
onRequest?: EventHandlerObject["onRequest"];
|
|
30
|
+
onBeforeResponse?: EventHandlerObject["onBeforeResponse"];
|
|
31
|
+
} = {}
|
|
32
|
+
) {
|
|
33
|
+
return eventHandler({
|
|
34
|
+
onRequest: options.onRequest,
|
|
35
|
+
onBeforeResponse: options.onBeforeResponse,
|
|
36
|
+
handler: (e: H3Event<EventHandlerRequest> & { startEvent: FetchEvent }) => {
|
|
37
|
+
const event = getFetchEvent(e);
|
|
38
|
+
|
|
39
|
+
return provideRequestEvent(event, async () => {
|
|
40
|
+
// api
|
|
41
|
+
const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method as HTTPMethod)
|
|
42
|
+
if (match) {
|
|
43
|
+
const mod = await match.handler.import();
|
|
44
|
+
const fn = mod[event.request.method];
|
|
45
|
+
(event as APIEvent).params = match.params;
|
|
46
|
+
sharedConfig.context = { event } as any;
|
|
47
|
+
return await fn(event);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// render stream
|
|
51
|
+
const doAsync = import.meta.env.START_SSR === "async";
|
|
52
|
+
const context = await createPageEvent(event);
|
|
53
|
+
let cloned = { ...options };
|
|
54
|
+
if (cloned.onCompleteAll) {
|
|
55
|
+
const og = cloned.onCompleteAll;
|
|
56
|
+
cloned.onCompleteAll = options => {
|
|
57
|
+
handleStreamCompleteRedirect(context)(options);
|
|
58
|
+
og(options);
|
|
59
|
+
};
|
|
60
|
+
} else cloned.onCompleteAll = handleStreamCompleteRedirect(context);
|
|
61
|
+
if (cloned.onCompleteShell) {
|
|
62
|
+
const og = cloned.onCompleteShell;
|
|
63
|
+
cloned.onCompleteShell = options => {
|
|
64
|
+
handleShellCompleteRedirect(context, e)();
|
|
65
|
+
og(options);
|
|
66
|
+
};
|
|
67
|
+
} else cloned.onCompleteShell = handleShellCompleteRedirect(context, e);
|
|
68
|
+
const stream = renderToStream(() => {
|
|
69
|
+
(sharedConfig.context as any).event = context;
|
|
70
|
+
return fn(context)
|
|
71
|
+
}, cloned);
|
|
72
|
+
if (context.response && context.response.headers.get("Location")) {
|
|
73
|
+
return sendRedirect(event, context.response.headers.get("Location"));
|
|
74
|
+
}
|
|
75
|
+
if (doAsync) return stream;
|
|
76
|
+
// fix cloudflare streaming
|
|
77
|
+
const { writable, readable } = new TransformStream();
|
|
78
|
+
stream.pipeTo(writable);
|
|
79
|
+
return readable;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleShellCompleteRedirect(context: PageEvent, e: H3Event<EventHandlerRequest>) {
|
|
86
|
+
return () => {
|
|
87
|
+
if (context.response && context.response.headers.get("Location")) {
|
|
88
|
+
setResponseStatus(e, 302);
|
|
89
|
+
setHeader(e, "Location", context.response.headers.get("Location"));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function handleStreamCompleteRedirect(context: PageEvent) {
|
|
95
|
+
return ({ write }: { write: (html: string) => void }) => {
|
|
96
|
+
const to = context.response && context.response.headers.get("Location");
|
|
97
|
+
to && write(`<script>window.location="${to}"</script>`);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createHandler(
|
|
102
|
+
fn: (context: PageEvent) => unknown,
|
|
103
|
+
options: Parameters<typeof createBaseHandler>[1] = {}
|
|
104
|
+
) {
|
|
105
|
+
return createBaseHandler(fn, { ...options, createPageEvent });
|
|
106
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createBaseHandler } from "..";
|
|
2
|
+
import { FetchEvent, PageEvent } from "../../server/types";
|
|
3
|
+
|
|
4
|
+
export function createHandler(
|
|
5
|
+
fn: (context: PageEvent) => unknown,
|
|
6
|
+
options?: { nonce?: string; renderId?: string; timeoutMs?: number }
|
|
7
|
+
) {
|
|
8
|
+
return createBaseHandler(fn, { ...options, createPageEvent });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function createPageEvent(ctx: FetchEvent) {
|
|
12
|
+
const clientManifest = import.meta.env.MANIFEST["client"];
|
|
13
|
+
const pageEvent: PageEvent = Object.assign(ctx, {
|
|
14
|
+
manifest: await clientManifest.json(),
|
|
15
|
+
assets: [...(await clientManifest.inputs[clientManifest.handler].assets())],
|
|
16
|
+
routes: [],
|
|
17
|
+
// prevUrl: "",
|
|
18
|
+
// mutation: false,
|
|
19
|
+
// $type: FETCH_EVENT,
|
|
20
|
+
$islands: new Set<string>()
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return pageEvent;
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidjs/start",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Ryan Carniato",
|
|
6
6
|
"publishConfig": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"client",
|
|
11
11
|
"config",
|
|
12
|
+
"entry",
|
|
12
13
|
"server",
|
|
13
14
|
"shared",
|
|
14
15
|
"*.d.ts"
|
|
@@ -42,8 +43,8 @@
|
|
|
42
43
|
}
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"solid-js": "^1.8.
|
|
46
|
-
"vinxi": "0.1.
|
|
46
|
+
"solid-js": "^1.8.11",
|
|
47
|
+
"vinxi": "^0.1.2"
|
|
47
48
|
},
|
|
48
49
|
"dependencies": {
|
|
49
50
|
"@vinxi/plugin-directives": "^0.1.0",
|
package/shared/routes.ts
CHANGED
|
@@ -33,7 +33,7 @@ export const pageRoutes = defineRoutes((fileRoutes as unknown as Route[]).filter
|
|
|
33
33
|
const apiRoutes = defineAPIRoutes((fileRoutes as unknown as Route[]).filter(o => o.type === "api"));
|
|
34
34
|
|
|
35
35
|
export function matchAPIRoute(path: string, method: HTTPMethod) {
|
|
36
|
-
const segments = path.split("/").filter(Boolean);
|
|
36
|
+
const segments = path.replace(import.meta.env.SERVER_BASE_URL, "").split("/").filter(Boolean);
|
|
37
37
|
|
|
38
38
|
routeLoop: for (const route of apiRoutes) {
|
|
39
39
|
const matchSegments = route.matchSegments;
|