@solidjs/start 0.4.0-alpha.0 → 0.4.0-alpha.10
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/README.md +12 -0
- package/client/StartClient.tsx +4 -1
- package/config/index.d.ts +14 -0
- package/config/index.js +31 -23
- package/config/server-fns-runtime.jsx +22 -0
- package/config/server-handler.js +113 -0
- package/config/server-runtime.jsx +78 -14
- package/env.d.ts +23 -0
- package/package.json +12 -8
- package/server/StartServer.tsx +45 -7
- package/server/handler.tsx +57 -35
- package/server/index.tsx +12 -2
- package/server/middleware.ts +18 -43
- package/server/page-event.tsx +78 -0
- package/server/spa/StartServer.tsx +3 -2
- package/server/spa/handler.tsx +4 -4
- package/server/spa/index.tsx +2 -0
- package/server/types.ts +22 -33
- package/shared/FileRoutes.tsx +4 -1
- package/shared/HttpHeader.tsx +2 -21
- package/shared/HttpStatusCode.tsx +2 -5
- package/shared/index.tsx +0 -1
- package/shared/lazyRoute.tsx +2 -1
- package/shared/routes.ts +145 -11
package/README.md
ADDED
package/client/StartClient.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @ts-ignore
|
|
2
2
|
import App from "#start/app";
|
|
3
|
+
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
3
4
|
import "./mount";
|
|
4
5
|
|
|
5
6
|
function Dummy(props) {
|
|
@@ -10,7 +11,9 @@ export function StartClient() {
|
|
|
10
11
|
return (
|
|
11
12
|
<Dummy>
|
|
12
13
|
<Dummy>
|
|
13
|
-
<
|
|
14
|
+
<ErrorBoundary>
|
|
15
|
+
<App />
|
|
16
|
+
</ErrorBoundary>
|
|
14
17
|
</Dummy>
|
|
15
18
|
</Dummy>
|
|
16
19
|
);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { InlineConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
type SolidStartInlineConfig = Omit<InlineConfig, "router"> & {
|
|
4
|
+
start?: {
|
|
5
|
+
ssr?: boolean | "async",
|
|
6
|
+
extensions?: string[],
|
|
7
|
+
server?: any,
|
|
8
|
+
appRoot?: string,
|
|
9
|
+
middleware?: string,
|
|
10
|
+
//islands?: boolean TODO: Get PROD bugs working probably missing release
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export declare function defineConfig(baseConfig: SolidStartInlineConfig)
|
package/config/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { serverFunctions } from "@vinxi/server-functions/plugin";
|
|
2
|
+
import { serverTransform } from "@vinxi/server-functions/server";
|
|
2
3
|
import defu from "defu";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -42,13 +43,19 @@ export function defineConfig(baseConfig = {}) {
|
|
|
42
43
|
appRoot: "./src",
|
|
43
44
|
ssr: true,
|
|
44
45
|
islands: false,
|
|
45
|
-
|
|
46
|
+
solid: {}
|
|
46
47
|
});
|
|
48
|
+
let server = start.server;
|
|
49
|
+
if (!start.ssr) {
|
|
50
|
+
server = { ...server, prerender: { routes: ["/"] } };
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
return createApp({
|
|
48
54
|
server: {
|
|
49
55
|
compressPublicAssets: {
|
|
50
56
|
brotli: true
|
|
51
|
-
}
|
|
57
|
+
},
|
|
58
|
+
...server
|
|
52
59
|
},
|
|
53
60
|
routers: [
|
|
54
61
|
{
|
|
@@ -63,16 +70,17 @@ export function defineConfig(baseConfig = {}) {
|
|
|
63
70
|
mode: "handler",
|
|
64
71
|
handler: `${start.appRoot}/entry-server.tsx`,
|
|
65
72
|
middleware: start.middleware,
|
|
66
|
-
|
|
67
|
-
? { routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }) }
|
|
68
|
-
: {}),
|
|
73
|
+
routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
|
|
69
74
|
extensions,
|
|
70
75
|
target: "server",
|
|
71
76
|
plugins: () => [
|
|
72
77
|
config("user", userConfig),
|
|
73
78
|
...plugins,
|
|
79
|
+
serverTransform({
|
|
80
|
+
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.jsx", import.meta.url)))
|
|
81
|
+
}),
|
|
74
82
|
start.islands ? serverComponents.server() : null,
|
|
75
|
-
solid({ ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
83
|
+
solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
76
84
|
config("app-server", {
|
|
77
85
|
resolve: {
|
|
78
86
|
alias: {
|
|
@@ -82,13 +90,15 @@ export function defineConfig(baseConfig = {}) {
|
|
|
82
90
|
? {
|
|
83
91
|
"@solidjs/start/server": "@solidjs/start/server/spa"
|
|
84
92
|
}
|
|
85
|
-
: {})
|
|
93
|
+
: {}),
|
|
94
|
+
...userConfig.resolve?.alias
|
|
86
95
|
}
|
|
87
96
|
},
|
|
88
97
|
define: {
|
|
89
98
|
"import.meta.env.START_ISLANDS": JSON.stringify(start.islands),
|
|
90
99
|
"import.meta.env.SSR": JSON.stringify(true),
|
|
91
|
-
"import.meta.env.START_SSR": JSON.stringify(start.ssr)
|
|
100
|
+
"import.meta.env.START_SSR": JSON.stringify(start.ssr),
|
|
101
|
+
...userConfig.define
|
|
92
102
|
}
|
|
93
103
|
})
|
|
94
104
|
]
|
|
@@ -111,7 +121,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
111
121
|
runtime: normalize(fileURLToPath(new URL("./server-runtime.jsx", import.meta.url)))
|
|
112
122
|
}),
|
|
113
123
|
start.islands ? serverComponents.client() : null,
|
|
114
|
-
solid({ ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
|
|
124
|
+
solid({ ...start.solid, ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
|
|
115
125
|
config("app-client", {
|
|
116
126
|
resolve: {
|
|
117
127
|
alias: {
|
|
@@ -126,32 +136,28 @@ export function defineConfig(baseConfig = {}) {
|
|
|
126
136
|
? {
|
|
127
137
|
"@solidjs/start/client": "@solidjs/start/client/spa"
|
|
128
138
|
}
|
|
129
|
-
: {})
|
|
139
|
+
: {}),
|
|
140
|
+
...userConfig.resolve?.alias
|
|
130
141
|
}
|
|
131
142
|
},
|
|
132
143
|
define: {
|
|
133
144
|
"import.meta.env.START_ISLANDS": JSON.stringify(start.islands),
|
|
134
145
|
"import.meta.env.SSR": JSON.stringify(false),
|
|
135
|
-
"import.meta.env.START_SSR": JSON.stringify(start.ssr)
|
|
146
|
+
"import.meta.env.START_SSR": JSON.stringify(start.ssr),
|
|
147
|
+
...userConfig.define
|
|
136
148
|
}
|
|
137
149
|
})
|
|
138
150
|
],
|
|
139
151
|
base: "/_build"
|
|
140
152
|
},
|
|
141
|
-
// {
|
|
142
|
-
// name: "server",
|
|
143
|
-
// mode: "handler",
|
|
144
|
-
// base: "/_server",
|
|
145
|
-
// handler: serverFunctions.handler,
|
|
146
|
-
// target: "server",
|
|
147
|
-
// ...(overrides ?? {}),
|
|
148
|
-
// plugins: () => [serverserver(), ...(overrides?.plugins?.() ?? [])],
|
|
149
|
-
// },
|
|
150
153
|
serverFunctions.router({
|
|
154
|
+
handler: normalize(fileURLToPath(new URL("./server-handler.js", import.meta.url))),
|
|
155
|
+
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.jsx", import.meta.url))),
|
|
156
|
+
// routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
|
|
151
157
|
plugins: () => [
|
|
152
158
|
config("user", userConfig),
|
|
153
159
|
...plugins,
|
|
154
|
-
solid({ ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
160
|
+
solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
155
161
|
config("app-server", {
|
|
156
162
|
resolve: {
|
|
157
163
|
alias: {
|
|
@@ -161,13 +167,15 @@ export function defineConfig(baseConfig = {}) {
|
|
|
161
167
|
? {
|
|
162
168
|
"@solidjs/start/server": "@solidjs/start/server/spa"
|
|
163
169
|
}
|
|
164
|
-
: {})
|
|
170
|
+
: {}),
|
|
171
|
+
...userConfig.resolve?.alias
|
|
165
172
|
}
|
|
166
173
|
},
|
|
167
174
|
define: {
|
|
168
175
|
"import.meta.env.START_ISLANDS": JSON.stringify(start.islands),
|
|
169
176
|
"import.meta.env.SSR": JSON.stringify(true),
|
|
170
|
-
"import.meta.env.START_SSR": JSON.stringify(start.ssr)
|
|
177
|
+
"import.meta.env.START_SSR": JSON.stringify(start.ssr),
|
|
178
|
+
...userConfig.define
|
|
171
179
|
}
|
|
172
180
|
})
|
|
173
181
|
],
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getRequestEvent } from "solid-js/web";
|
|
2
|
+
import { provideRequestEvent } from "solid-js/web/storage";
|
|
3
|
+
import { cloneEvent } from "../server";
|
|
4
|
+
|
|
5
|
+
export function createServerReference(fn, id, name) {
|
|
6
|
+
return new Proxy(fn, {
|
|
7
|
+
get(target, prop, receiver) {
|
|
8
|
+
if (prop === "url") {
|
|
9
|
+
return `/_server?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`;
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
apply(target, thisArg, args) {
|
|
13
|
+
const ogEvt = getRequestEvent();
|
|
14
|
+
if (!ogEvt) throw new Error("Cannot call server function outside of a request");
|
|
15
|
+
const evt = cloneEvent(ogEvt);
|
|
16
|
+
evt.serverOnly = true;
|
|
17
|
+
return provideRequestEvent(evt, () => {
|
|
18
|
+
return fn.apply(thisArg, args);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/// <reference types="vinxi/types/server" />
|
|
2
|
+
import { crossSerializeStream, fromJSON } from "seroval";
|
|
3
|
+
import { provideRequestEvent } from "solid-js/web/storage";
|
|
4
|
+
import invariant from "vinxi/lib/invariant";
|
|
5
|
+
import {
|
|
6
|
+
eventHandler,
|
|
7
|
+
getHeader,
|
|
8
|
+
getRequestURL,
|
|
9
|
+
readBody,
|
|
10
|
+
readFormData,
|
|
11
|
+
setHeader
|
|
12
|
+
} from "vinxi/server";
|
|
13
|
+
import { getFetchEvent } from "../server/middleware";
|
|
14
|
+
|
|
15
|
+
function serializeToStream(id, value) {
|
|
16
|
+
return new ReadableStream({
|
|
17
|
+
start(controller) {
|
|
18
|
+
crossSerializeStream(value, {
|
|
19
|
+
scopeId: id,
|
|
20
|
+
onSerialize(data, initial) {
|
|
21
|
+
const result = initial ? `($R["${id}"]=[],${data})` : data;
|
|
22
|
+
controller.enqueue(new TextEncoder().encode(`${result};\n`));
|
|
23
|
+
},
|
|
24
|
+
onDone() {
|
|
25
|
+
// controller.enqueue(`delete $R["${id}"];\n`);
|
|
26
|
+
controller.close();
|
|
27
|
+
},
|
|
28
|
+
onError(error) {
|
|
29
|
+
// controller.enqueue(`delete $R["${id}"];\n`);
|
|
30
|
+
controller.error(error);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function handleServerFunction(event) {
|
|
38
|
+
invariant(event.method === "POST", `Invalid method ${event.method}. Expected POST.`);
|
|
39
|
+
|
|
40
|
+
const serverReference = getHeader(event, "x-server-id");
|
|
41
|
+
const instance = getHeader(event, "x-server-instance");
|
|
42
|
+
const url = getRequestURL(event);
|
|
43
|
+
let filepath, name;
|
|
44
|
+
if (serverReference) {
|
|
45
|
+
invariant(typeof serverReference === "string", "Invalid server function");
|
|
46
|
+
[filepath, name] = serverReference.split("#");
|
|
47
|
+
} else {
|
|
48
|
+
filepath = url.searchParams.get("id");
|
|
49
|
+
name = url.searchParams.get("name");
|
|
50
|
+
if (!filepath || !name) throw new Error("Invalid request");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const action = (
|
|
54
|
+
await import.meta.env.MANIFEST[import.meta.env.ROUTER_NAME].chunks[filepath].import()
|
|
55
|
+
)[name];
|
|
56
|
+
let parsed = [];
|
|
57
|
+
|
|
58
|
+
// grab bound arguments from url when no JS
|
|
59
|
+
if (!instance) {
|
|
60
|
+
const args = url.searchParams.get("args");
|
|
61
|
+
if (args) JSON.parse(args).forEach(arg => parsed.push(arg));
|
|
62
|
+
}
|
|
63
|
+
const contentType = getHeader(event, "content-type");
|
|
64
|
+
if (
|
|
65
|
+
contentType.startsWith("multipart/form-data") ||
|
|
66
|
+
contentType.startsWith("application/x-www-form-urlencoded")
|
|
67
|
+
) {
|
|
68
|
+
parsed.push(await readFormData(event));
|
|
69
|
+
} else {
|
|
70
|
+
parsed = fromJSON(await readBody(event));
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const result = await provideRequestEvent(getFetchEvent(event), () => action(...parsed));
|
|
74
|
+
|
|
75
|
+
// handle no JS success case
|
|
76
|
+
if (!instance) {
|
|
77
|
+
const isError = result instanceof Error;
|
|
78
|
+
const refererUrl = new URL(getHeader(event, "referer"));
|
|
79
|
+
return new Response(null, {
|
|
80
|
+
status: 302,
|
|
81
|
+
headers: {
|
|
82
|
+
Location: refererUrl.toString(),
|
|
83
|
+
...(result ? { "Set-Cookie": `flash=${JSON.stringify({
|
|
84
|
+
url: url.pathname + encodeURIComponent(url.search),
|
|
85
|
+
result: isError ? result.message : result,
|
|
86
|
+
error: isError,
|
|
87
|
+
input: [...parsed.slice(0, -1), [...parsed[parsed.length-1].entries()]]
|
|
88
|
+
})}; Secure; HttpOnly;`} : {})
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setHeader(event, "content-type", "text/javascript");
|
|
94
|
+
return serializeToStream(instance, result);
|
|
95
|
+
} catch (x) {
|
|
96
|
+
if (x instanceof Response && x.status === 302) {
|
|
97
|
+
return new Response(null, {
|
|
98
|
+
status: instance ? 204 : 302,
|
|
99
|
+
headers: {
|
|
100
|
+
Location: x.headers.get("Location")
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return new Response(serializeToStream(instance, x), {
|
|
105
|
+
status: 500,
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "text/javascript"
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default eventHandler(handleServerFunction);
|
|
@@ -1,25 +1,89 @@
|
|
|
1
|
+
import { deserialize, toJSONAsync } from "seroval";
|
|
1
2
|
import { createIslandReference } from "../server/islands";
|
|
2
3
|
|
|
3
|
-
async function
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
async function deserializeStream(id, response) {
|
|
5
|
+
if (!response.body) {
|
|
6
|
+
throw new Error("missing body");
|
|
7
|
+
}
|
|
8
|
+
const reader = response.body.getReader();
|
|
9
|
+
|
|
10
|
+
async function pop() {
|
|
11
|
+
const result = await reader.read();
|
|
12
|
+
if (!result.done) {
|
|
13
|
+
const serialized = new TextDecoder().decode(result.value);
|
|
14
|
+
const splits = serialized.split("\n");
|
|
15
|
+
for (const split of splits) {
|
|
16
|
+
if (split !== "") {
|
|
17
|
+
deserialize(split);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
await pop();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const result = await reader.read();
|
|
25
|
+
if (result.done) {
|
|
26
|
+
throw new Error("Unexpected end of body");
|
|
27
|
+
}
|
|
28
|
+
const serialized = new TextDecoder().decode(result.value);
|
|
29
|
+
let pending = true;
|
|
30
|
+
let revived;
|
|
31
|
+
const splits = serialized.split("\n");
|
|
32
|
+
for (const split of splits) {
|
|
33
|
+
if (split !== "") {
|
|
34
|
+
const current = deserialize(split);
|
|
35
|
+
if (pending) {
|
|
36
|
+
revived = current;
|
|
37
|
+
pending = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pop().then(
|
|
43
|
+
() => {
|
|
44
|
+
delete self.$R[id];
|
|
45
|
+
},
|
|
46
|
+
() => {
|
|
47
|
+
// no-op
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return revived;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let INSTANCE = 0;
|
|
55
|
+
|
|
56
|
+
function createRequest(base, id, instance, body, contentType) {
|
|
57
|
+
return fetch(base, {
|
|
6
58
|
method: "POST",
|
|
7
59
|
headers: {
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
"
|
|
60
|
+
"x-server-id": id,
|
|
61
|
+
"x-server-instance": instance,
|
|
62
|
+
...(contentType ? { "Content-Type": contentType } : {})
|
|
11
63
|
},
|
|
12
|
-
body
|
|
64
|
+
body
|
|
13
65
|
});
|
|
66
|
+
}
|
|
14
67
|
|
|
15
|
-
|
|
68
|
+
async function fetchServerFunction(base, id, args) {
|
|
69
|
+
const instance = `server-fn:${INSTANCE++}`;
|
|
70
|
+
const response = await (args.length === 1 && args[0] instanceof FormData
|
|
71
|
+
? createRequest(base, id, instance, args[0])
|
|
72
|
+
: createRequest(base, id, instance, JSON.stringify(await Promise.resolve(toJSONAsync(args))), "application/json"));
|
|
16
73
|
|
|
17
|
-
if (response.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
74
|
+
if (response.headers.get("Location")) throw response;
|
|
75
|
+
if (response.headers.get("X-Revalidate")) {
|
|
76
|
+
/* ts-ignore-next-line */
|
|
77
|
+
response.customBody = () => {
|
|
78
|
+
return deserializeStream(instance, response);
|
|
79
|
+
}
|
|
80
|
+
throw response;
|
|
81
|
+
}
|
|
82
|
+
const result = deserializeStream(instance, response);
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
return result;
|
|
22
85
|
}
|
|
86
|
+
throw result;
|
|
23
87
|
}
|
|
24
88
|
|
|
25
89
|
export function createServerReference(fn, id, name) {
|
|
@@ -30,7 +94,7 @@ export function createServerReference(fn, id, name) {
|
|
|
30
94
|
}
|
|
31
95
|
},
|
|
32
96
|
apply(target, thisArg, args) {
|
|
33
|
-
return
|
|
97
|
+
return fetchServerFunction("/_server", `${id}#${name}`, args);
|
|
34
98
|
}
|
|
35
99
|
});
|
|
36
100
|
}
|
package/env.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// This file is an augmentation to the built-in ImportMeta interface
|
|
2
|
+
// Thus cannot contain any top-level imports
|
|
3
|
+
// <https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation>
|
|
4
|
+
|
|
5
|
+
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
|
6
|
+
|
|
7
|
+
interface ImportMetaEnv extends Record<`VITE_${string}`, any>, SolidStartMetaEnv {
|
|
8
|
+
BASE_URL: string;
|
|
9
|
+
MODE: string;
|
|
10
|
+
DEV: boolean;
|
|
11
|
+
PROD: boolean;
|
|
12
|
+
SSR: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SolidStartMetaEnv {
|
|
16
|
+
START_ISLANDS_ROUTER: string;
|
|
17
|
+
START_INDEX_HTML: string;
|
|
18
|
+
START_ENTRY_CLIENT: string;
|
|
19
|
+
START_SSR: string;
|
|
20
|
+
START_ISLANDS: string;
|
|
21
|
+
START_ENTRY_SERVER: string;
|
|
22
|
+
DEBUG: string;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidjs/start",
|
|
3
|
-
"version": "0.4.0-alpha.
|
|
3
|
+
"version": "0.4.0-alpha.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Ryan Carniato",
|
|
6
6
|
"publishConfig": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"shared",
|
|
14
14
|
"*.d.ts"
|
|
15
15
|
],
|
|
16
|
-
"types": "./index.tsx",
|
|
16
|
+
"types": "./shared/index.tsx",
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./shared/index.tsx",
|
|
19
19
|
"./config": "./config/index.js",
|
|
@@ -38,15 +38,19 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"solid-js": "^1.8.
|
|
42
|
-
"vinxi": "0.0.
|
|
41
|
+
"solid-js": "^1.8.7",
|
|
42
|
+
"vinxi": "0.0.52"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@vinxi/plugin-directives": "0.0.
|
|
46
|
-
"@vinxi/server-
|
|
47
|
-
"@vinxi/server-
|
|
45
|
+
"@vinxi/plugin-directives": "0.0.44",
|
|
46
|
+
"@vinxi/server-components": "0.0.44",
|
|
47
|
+
"@vinxi/server-functions": "0.0.45",
|
|
48
48
|
"defu": "^6.1.2",
|
|
49
|
+
"seroval": "^0.14.1",
|
|
49
50
|
"vite-plugin-inspect": "^0.7.33",
|
|
50
|
-
"vite-plugin-solid": "^2.
|
|
51
|
+
"vite-plugin-solid": "^2.8.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"typecheck": "tsc --noEmit --checkJs false --skipLibCheck"
|
|
51
55
|
}
|
|
52
56
|
}
|
package/server/StartServer.tsx
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
// @ts-ignore
|
|
2
2
|
import App from "#start/app";
|
|
3
|
-
import {
|
|
3
|
+
import type { Component } from "solid-js";
|
|
4
|
+
import {
|
|
5
|
+
Hydration,
|
|
6
|
+
HydrationScript,
|
|
7
|
+
NoHydration,
|
|
8
|
+
getRequestEvent,
|
|
9
|
+
ssr,
|
|
10
|
+
useAssets
|
|
11
|
+
} from "solid-js/web";
|
|
12
|
+
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
4
13
|
import { renderAsset } from "./renderAsset";
|
|
14
|
+
import type { DocumentComponentProps } from "./types";
|
|
5
15
|
|
|
6
16
|
const docType = ssr("<!DOCTYPE html>");
|
|
7
17
|
|
|
8
|
-
export function StartServer(props) {
|
|
9
|
-
const context =
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
export function StartServer(props: { document: Component<DocumentComponentProps> }) {
|
|
19
|
+
const context = getRequestEvent() as any;
|
|
20
|
+
|
|
21
|
+
let assets = [];
|
|
22
|
+
Promise.resolve().then(async () => {
|
|
23
|
+
let current = context.routes;
|
|
24
|
+
if (context.routerMatches && context.routerMatches[0]) {
|
|
25
|
+
for (let i = 0; i < context.routerMatches[0].length; i++) {
|
|
26
|
+
const match = context.routerMatches[0][i];
|
|
27
|
+
if (match.metadata && match.metadata.filesystem) {
|
|
28
|
+
const segment = current.find(r => r.path === match.path);
|
|
29
|
+
const part = import.meta.env.MANIFEST["client"].inputs[segment["$component"].src];
|
|
30
|
+
const asset = await part.assets();
|
|
31
|
+
assets.push.apply(assets, asset);
|
|
32
|
+
current = segment.children;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// dedupe assets
|
|
37
|
+
assets = [...new Map(assets.map(item => [item.attrs.key, item])).values()].filter(
|
|
38
|
+
asset =>
|
|
39
|
+
asset.attrs.rel === "modulepreload" &&
|
|
40
|
+
!context.assets.find(a => a.attrs.key === asset.attrs.key)
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
useAssets(() => assets.length ? assets.map(m => renderAsset(m)) : undefined);
|
|
45
|
+
|
|
12
46
|
return (
|
|
13
47
|
<NoHydration>
|
|
14
48
|
{docType as unknown as any}
|
|
@@ -31,10 +65,14 @@ export function StartServer(props) {
|
|
|
31
65
|
>
|
|
32
66
|
{!import.meta.env.START_ISLANDS ? (
|
|
33
67
|
<Hydration>
|
|
34
|
-
<
|
|
68
|
+
<ErrorBoundary>
|
|
69
|
+
<App />
|
|
70
|
+
</ErrorBoundary>
|
|
35
71
|
</Hydration>
|
|
36
72
|
) : (
|
|
37
|
-
<
|
|
73
|
+
<ErrorBoundary>
|
|
74
|
+
<App />
|
|
75
|
+
</ErrorBoundary>
|
|
38
76
|
)}
|
|
39
77
|
</props.document>
|
|
40
78
|
</NoHydration>
|
package/server/handler.tsx
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { renderToStream } from "solid-js/web";
|
|
2
|
+
/* @ts-ignore */
|
|
2
3
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
EventHandlerObject,
|
|
6
|
+
EventHandlerRequest,
|
|
7
|
+
H3Event,
|
|
8
|
+
HTTPMethod,
|
|
9
|
+
eventHandler,
|
|
10
|
+
sendRedirect,
|
|
11
|
+
setHeader,
|
|
12
|
+
setResponseStatus
|
|
13
|
+
} from "vinxi/server";
|
|
14
|
+
import { matchAPIRoute } from "../shared/routes";
|
|
6
15
|
import { getFetchEvent } from "./middleware";
|
|
7
|
-
import {
|
|
16
|
+
import { createPageEvent } from "./page-event";
|
|
17
|
+
import { APIEvent, FetchEvent, PageEvent } from "./types";
|
|
8
18
|
|
|
9
19
|
export function createHandler(
|
|
10
20
|
fn: (context: PageEvent) => unknown,
|
|
@@ -12,6 +22,8 @@ export function createHandler(
|
|
|
12
22
|
nonce?: string;
|
|
13
23
|
renderId?: string;
|
|
14
24
|
timeoutMs?: number;
|
|
25
|
+
onCompleteAll?: (options: { write: (v: any) => void }) => void;
|
|
26
|
+
onCompleteShell?: (options: { write: (v: any) => void }) => void;
|
|
15
27
|
createPageEvent?: (event: FetchEvent) => Promise<PageEvent>;
|
|
16
28
|
onRequest?: EventHandlerObject["onRequest"];
|
|
17
29
|
onBeforeResponse?: EventHandlerObject["onBeforeResponse"];
|
|
@@ -25,48 +37,58 @@ export function createHandler(
|
|
|
25
37
|
|
|
26
38
|
return provideRequestEvent(event, async () => {
|
|
27
39
|
// api
|
|
28
|
-
const match =
|
|
29
|
-
route =>
|
|
30
|
-
route[`$${event.request.method}`] && new URL(event.request.url).pathname === route.path
|
|
31
|
-
);
|
|
40
|
+
const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method as HTTPMethod)
|
|
32
41
|
if (match) {
|
|
33
|
-
const mod = await match
|
|
42
|
+
const mod = await match.handler.import();
|
|
34
43
|
const fn = mod[event.request.method];
|
|
35
|
-
|
|
36
|
-
return
|
|
44
|
+
(event as APIEvent).params = match.params;
|
|
45
|
+
return await fn(event);
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
// render stream
|
|
49
|
+
const doAsync = import.meta.env.START_SSR === "async";
|
|
40
50
|
const context = await createPageEvent(event);
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
51
|
+
let cloned = { ...options };
|
|
52
|
+
if (cloned.onCompleteAll) {
|
|
53
|
+
const og = cloned.onCompleteAll;
|
|
54
|
+
cloned.onCompleteAll = options => {
|
|
55
|
+
handleStreamCompleteRedirect(context)(options);
|
|
56
|
+
og(options);
|
|
57
|
+
};
|
|
58
|
+
} else cloned.onCompleteAll = handleStreamCompleteRedirect(context);
|
|
59
|
+
if (cloned.onCompleteShell) {
|
|
60
|
+
const og = cloned.onCompleteShell;
|
|
61
|
+
cloned.onCompleteShell = options => {
|
|
62
|
+
handleShellCompleteRedirect(context, e)();
|
|
63
|
+
og(options);
|
|
64
|
+
};
|
|
65
|
+
} else cloned.onCompleteShell = handleShellCompleteRedirect(context, e);
|
|
66
|
+
const stream = renderToStream(() => fn(context), cloned);
|
|
67
|
+
if (context.response && context.response.headers.get("Location")) {
|
|
68
|
+
return sendRedirect(event, context.response.headers.get("Location"));
|
|
44
69
|
}
|
|
45
|
-
|
|
70
|
+
if (doAsync) return stream;
|
|
71
|
+
// fix cloudflare streaming
|
|
72
|
+
const { writable, readable } = new TransformStream();
|
|
73
|
+
stream.pipeTo(writable);
|
|
74
|
+
return readable;
|
|
46
75
|
});
|
|
47
76
|
}
|
|
48
77
|
});
|
|
49
78
|
}
|
|
50
79
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
...(await clientManifest.inputs[clientManifest.handler].assets()),
|
|
60
|
-
...(import.meta.env.START_ISLANDS
|
|
61
|
-
? await serverManifest.inputs[serverManifest.handler].assets()
|
|
62
|
-
: [])
|
|
63
|
-
],
|
|
64
|
-
routes: createRoutes(),
|
|
65
|
-
prevUrl: prevPath || "",
|
|
66
|
-
mutation: mutation,
|
|
67
|
-
$type: FETCH_EVENT,
|
|
68
|
-
$islands: new Set<string>()
|
|
69
|
-
});
|
|
80
|
+
function handleShellCompleteRedirect(context: PageEvent, e: H3Event<EventHandlerRequest>) {
|
|
81
|
+
return () => {
|
|
82
|
+
if (context.response && context.response.headers.get("Location")) {
|
|
83
|
+
setResponseStatus(e, 302);
|
|
84
|
+
setHeader(e, "Location", context.response.headers.get("Location"));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
70
88
|
|
|
71
|
-
|
|
89
|
+
function handleStreamCompleteRedirect(context: PageEvent) {
|
|
90
|
+
return ({ write }: { write: (html: string) => void }) => {
|
|
91
|
+
const to = context.response && context.response.headers.get("Location");
|
|
92
|
+
to && write(`<script>window.location="${to}"</script>`);
|
|
93
|
+
};
|
|
72
94
|
}
|
package/server/index.tsx
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
export * from "vinxi/server";
|
|
1
2
|
export { StartServer } from "./StartServer";
|
|
2
|
-
export { createHandler } from "./handler";
|
|
3
3
|
export * from "./middleware";
|
|
4
|
-
export
|
|
4
|
+
export * from "./types";
|
|
5
|
+
import { createHandler as createBaseHandler } from "./handler";
|
|
6
|
+
import { createPageEvent } from "./page-event";
|
|
7
|
+
import type { PageEvent } from "./types";
|
|
8
|
+
|
|
9
|
+
export function createHandler(
|
|
10
|
+
fn: (context: PageEvent) => unknown,
|
|
11
|
+
options: Parameters<typeof createBaseHandler>[1] = {}
|
|
12
|
+
) {
|
|
13
|
+
return createBaseHandler(fn, { ...options, createPageEvent });
|
|
14
|
+
}
|
package/server/middleware.ts
CHANGED
|
@@ -1,45 +1,37 @@
|
|
|
1
1
|
import {
|
|
2
|
-
appendResponseHeader,
|
|
3
2
|
defineMiddleware,
|
|
4
3
|
EventHandlerRequest,
|
|
5
4
|
getRequestIP,
|
|
6
|
-
getResponseHeader,
|
|
7
|
-
getResponseStatus,
|
|
8
5
|
H3Event,
|
|
9
|
-
removeResponseHeader,
|
|
10
|
-
sendRedirect,
|
|
11
6
|
sendWebResponse,
|
|
12
|
-
setResponseHeader,
|
|
13
|
-
setResponseStatus,
|
|
14
7
|
toWebRequest
|
|
15
8
|
} from "vinxi/server";
|
|
16
9
|
import { FetchEvent } from "./types";
|
|
17
10
|
|
|
18
|
-
export * from "vinxi/server";
|
|
19
|
-
|
|
20
11
|
const h3EventSymbol = Symbol("h3Event");
|
|
21
12
|
const fetchEventSymbol = Symbol("fetchEvent");
|
|
13
|
+
const eventTraps = {
|
|
14
|
+
get(target, prop) {
|
|
15
|
+
if (prop === fetchEventSymbol) return target;
|
|
16
|
+
return target[prop] ?? target[h3EventSymbol][prop];
|
|
17
|
+
}
|
|
18
|
+
};
|
|
22
19
|
|
|
23
20
|
export function createFetchEvent(event: H3Event<EventHandlerRequest>): FetchEvent {
|
|
24
|
-
return
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
removeResponseHeader: name => removeResponseHeader(event, name),
|
|
35
|
-
// @ts-ignore
|
|
36
|
-
[h3EventSymbol]: event
|
|
37
|
-
};
|
|
21
|
+
return new Proxy(
|
|
22
|
+
{
|
|
23
|
+
request: toWebRequest(event),
|
|
24
|
+
clientAddress: getRequestIP(event),
|
|
25
|
+
locals: {},
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
[h3EventSymbol]: event
|
|
28
|
+
},
|
|
29
|
+
eventTraps
|
|
30
|
+
) as unknown as FetchEvent;
|
|
38
31
|
}
|
|
39
32
|
|
|
40
|
-
export function
|
|
41
|
-
|
|
42
|
-
return fetchEvent[h3EventSymbol];
|
|
33
|
+
export function cloneEvent<T extends FetchEvent>(fetchEvent: T): T {
|
|
34
|
+
return new Proxy({ ...fetchEvent[fetchEventSymbol] }, eventTraps);
|
|
43
35
|
}
|
|
44
36
|
|
|
45
37
|
export function getFetchEvent(h3Event: H3Event): FetchEvent {
|
|
@@ -52,28 +44,11 @@ export function getFetchEvent(h3Event: H3Event): FetchEvent {
|
|
|
52
44
|
return h3Event[fetchEventSymbol];
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
export type Middleware = (input: MiddlewareInput) => MiddlewareFn;
|
|
56
|
-
/** Input parameters for to an Exchange factory function. */
|
|
57
|
-
|
|
58
|
-
export interface MiddlewareInput {
|
|
59
|
-
forward: MiddlewareFn;
|
|
60
|
-
}
|
|
61
47
|
/** Function responsible for receiving an observable [operation]{@link Operation} and returning a [result]{@link OperationResult}. */
|
|
62
48
|
|
|
63
49
|
export type MiddlewareFn = (event: FetchEvent) => Promise<unknown> | unknown;
|
|
64
50
|
/** This composes an array of Exchanges into a single ExchangeIO function */
|
|
65
51
|
|
|
66
|
-
export const composeMiddleware =
|
|
67
|
-
(exchanges: Middleware[]) =>
|
|
68
|
-
({ forward }: MiddlewareInput) =>
|
|
69
|
-
exchanges.reduceRight(
|
|
70
|
-
(forward, exchange) =>
|
|
71
|
-
exchange({
|
|
72
|
-
forward
|
|
73
|
-
}),
|
|
74
|
-
forward
|
|
75
|
-
);
|
|
76
|
-
|
|
77
52
|
type RequestMiddleware = (event: FetchEvent) => Response | Promise<Response> | void | Promise<void>;
|
|
78
53
|
|
|
79
54
|
type ResponseMiddleware = (
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendResponseHeader,
|
|
3
|
+
getCookie,
|
|
4
|
+
getResponseHeader,
|
|
5
|
+
removeResponseHeader,
|
|
6
|
+
setCookie,
|
|
7
|
+
setResponseHeader,
|
|
8
|
+
setResponseStatus
|
|
9
|
+
} from "vinxi/server";
|
|
10
|
+
import { createRoutes } from "../shared/FileRoutes";
|
|
11
|
+
import { FetchEvent, PageEvent } from "./types";
|
|
12
|
+
|
|
13
|
+
function initFromFlash(ctx: FetchEvent) {
|
|
14
|
+
const flash = getCookie(ctx, "flash");
|
|
15
|
+
if (!flash) return;
|
|
16
|
+
let param = JSON.parse(flash);
|
|
17
|
+
if (!param || !param.result) return [];
|
|
18
|
+
const input = [...param.input.slice(0, -1), new Map(param.input[param.input.length - 1])];
|
|
19
|
+
setCookie(ctx, "flash", "", { maxAge: 0 });
|
|
20
|
+
return {
|
|
21
|
+
url: param.url,
|
|
22
|
+
result: param.error ? new Error(param.result) : param.result,
|
|
23
|
+
input
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function createPageEvent(ctx: FetchEvent) {
|
|
28
|
+
const clientManifest = import.meta.env.MANIFEST["client"];
|
|
29
|
+
const serverManifest = import.meta.env.MANIFEST["ssr"];
|
|
30
|
+
setResponseHeader(ctx, "Content-Type", "text/html");
|
|
31
|
+
// const prevPath = ctx.request.headers.get("x-solid-referrer");
|
|
32
|
+
// const mutation = ctx.request.headers.get("x-solid-mutation") === "true";
|
|
33
|
+
const pageEvent: PageEvent = Object.assign(ctx, {
|
|
34
|
+
manifest: await clientManifest.json(),
|
|
35
|
+
assets: [
|
|
36
|
+
...(await clientManifest.inputs[clientManifest.handler].assets()),
|
|
37
|
+
...(import.meta.env.START_ISLANDS
|
|
38
|
+
? await serverManifest.inputs[serverManifest.handler].assets()
|
|
39
|
+
: [])
|
|
40
|
+
],
|
|
41
|
+
initialSubmission: initFromFlash(ctx),
|
|
42
|
+
routes: createRoutes(),
|
|
43
|
+
components: {
|
|
44
|
+
status: props => {
|
|
45
|
+
setResponseStatus(ctx, props.code, props.text);
|
|
46
|
+
return () => setResponseStatus(ctx, 200);
|
|
47
|
+
},
|
|
48
|
+
header: props => {
|
|
49
|
+
if (props.append) {
|
|
50
|
+
appendResponseHeader(ctx, props.name, props.value);
|
|
51
|
+
} else {
|
|
52
|
+
setResponseHeader(ctx, props.name, props.value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return () => {
|
|
56
|
+
const value = getResponseHeader(ctx, props.name);
|
|
57
|
+
|
|
58
|
+
// Todo: review this logic still the same in H3
|
|
59
|
+
// H3 supports arrays so its possible there is no need to split..
|
|
60
|
+
// typeof check here is to guard but ultimately may be unneeded
|
|
61
|
+
if (value && typeof value === "string") {
|
|
62
|
+
const values = value.split(", ");
|
|
63
|
+
const index = values.indexOf(props.value);
|
|
64
|
+
index !== -1 && values.splice(index, 1);
|
|
65
|
+
if (values.length) setResponseHeader(ctx, props.name, values.join(", "));
|
|
66
|
+
else removeResponseHeader(ctx, props.name);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
// prevUrl: prevPath || "",
|
|
72
|
+
// mutation: mutation,
|
|
73
|
+
// $type: FETCH_EVENT,
|
|
74
|
+
$islands: new Set<string>()
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return pageEvent;
|
|
78
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// @ts-ignore
|
|
2
|
-
import { NoHydration, ssr } from "solid-js/web";
|
|
2
|
+
import { NoHydration, getRequestEvent, ssr } from "solid-js/web";
|
|
3
3
|
import { renderAsset } from "../renderAsset";
|
|
4
4
|
|
|
5
5
|
const docType = ssr("<!DOCTYPE html>");
|
|
6
6
|
|
|
7
7
|
export function StartServer(props) {
|
|
8
|
-
const context =
|
|
8
|
+
const context = getRequestEvent() as any;
|
|
9
9
|
return (
|
|
10
10
|
<NoHydration>
|
|
11
11
|
{docType as unknown as any}
|
|
@@ -14,6 +14,7 @@ export function StartServer(props) {
|
|
|
14
14
|
scripts={
|
|
15
15
|
<>
|
|
16
16
|
<script innerHTML={`window.manifest = ${JSON.stringify(context.manifest)}`} />
|
|
17
|
+
<script>$R = [];</script>
|
|
17
18
|
<script
|
|
18
19
|
type="module"
|
|
19
20
|
src={
|
package/server/spa/handler.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHandler as createBaseHandler } from "../handler";
|
|
2
|
-
import { FetchEvent,
|
|
2
|
+
import { FetchEvent, PageEvent } from "../types";
|
|
3
3
|
|
|
4
4
|
export function createHandler(
|
|
5
5
|
fn: (context: PageEvent) => unknown,
|
|
@@ -14,9 +14,9 @@ export async function createPageEvent(ctx: FetchEvent) {
|
|
|
14
14
|
manifest: await clientManifest.json(),
|
|
15
15
|
assets: [...(await clientManifest.inputs[clientManifest.handler].assets())],
|
|
16
16
|
routes: [],
|
|
17
|
-
prevUrl: "",
|
|
18
|
-
mutation: false,
|
|
19
|
-
$type: FETCH_EVENT,
|
|
17
|
+
// prevUrl: "",
|
|
18
|
+
// mutation: false,
|
|
19
|
+
// $type: FETCH_EVENT,
|
|
20
20
|
$islands: new Set<string>()
|
|
21
21
|
});
|
|
22
22
|
|
package/server/spa/index.tsx
CHANGED
package/server/types.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { JSX } from "solid-js";
|
|
2
|
+
import { EventHandlerRequest, H3Event } from "vinxi/server";
|
|
2
3
|
|
|
3
|
-
export const FETCH_EVENT = "$FETCH";
|
|
4
|
+
// export const FETCH_EVENT = "$FETCH";
|
|
4
5
|
|
|
5
6
|
export type DocumentComponentProps = {
|
|
6
|
-
assets:
|
|
7
|
-
scripts:
|
|
7
|
+
assets: JSX.Element;
|
|
8
|
+
scripts: JSX.Element;
|
|
8
9
|
children: JSX.Element;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -15,44 +16,32 @@ export type ContextMatches = {
|
|
|
15
16
|
params: unknown;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
tag: string;
|
|
20
|
-
props: Record<string, unknown>;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type RouterContext = {
|
|
24
|
-
// router matches;
|
|
25
|
-
matches: ContextMatches[][];
|
|
26
|
-
// redirected url
|
|
27
|
-
url: string;
|
|
28
|
-
|
|
29
|
-
// server route fragments
|
|
30
|
-
replaceOutletId: string;
|
|
31
|
-
newOutletId: string;
|
|
32
|
-
partial: boolean;
|
|
33
|
-
nextRoute: any;
|
|
34
|
-
prevRoute: any;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export interface FetchEvent {
|
|
19
|
+
export interface FetchEvent extends H3Event<EventHandlerRequest> {
|
|
38
20
|
request: Request;
|
|
39
21
|
clientAddress: string;
|
|
40
22
|
locals: Record<string, unknown>;
|
|
41
|
-
redirect(url: string, status?: number): void;
|
|
42
|
-
getResponseStatus(): number;
|
|
43
|
-
setResponseStatus(code: number, text?: string): void;
|
|
44
|
-
getResponseHeader(name: string): string | number | string[];
|
|
45
|
-
setResponseHeader(name: string, value: string): void;
|
|
46
|
-
appendResponseHeader(name: string, value: string): void;
|
|
47
|
-
removeResponseHeader(name: string): void;
|
|
48
23
|
}
|
|
49
24
|
export interface PageEvent extends FetchEvent {
|
|
50
25
|
manifest: any;
|
|
51
26
|
assets: any;
|
|
52
27
|
routes: any[];
|
|
53
|
-
prevUrl: string | null;
|
|
54
|
-
$type: typeof FETCH_EVENT;
|
|
28
|
+
// prevUrl: string | null;
|
|
29
|
+
// $type: typeof FETCH_EVENT;
|
|
55
30
|
$islands: Set<string>;
|
|
56
|
-
mutation: boolean;
|
|
31
|
+
// mutation: boolean;
|
|
57
32
|
response?: Response;
|
|
58
33
|
}
|
|
34
|
+
|
|
35
|
+
export interface APIEvent extends FetchEvent {
|
|
36
|
+
params: { [key: string]: string };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface APIHandler {
|
|
40
|
+
(event: APIEvent): Promise<any>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare module "solid-js/web" {
|
|
44
|
+
interface RequestEvent extends FetchEvent {
|
|
45
|
+
serverOnly?: boolean;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/shared/FileRoutes.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
/* @refresh skip */
|
|
1
2
|
import { getRequestEvent, isServer } from "solid-js/web";
|
|
2
3
|
import lazyRoute from "./lazyRoute";
|
|
3
4
|
|
|
5
|
+
import { PageEvent } from "../server";
|
|
4
6
|
import { pageRoutes as routeConfigs } from "./routes";
|
|
5
7
|
|
|
6
8
|
export function createRoutes() {
|
|
@@ -8,6 +10,7 @@ export function createRoutes() {
|
|
|
8
10
|
return {
|
|
9
11
|
...route,
|
|
10
12
|
...(route.$$route ? route.$$route.require().route : undefined),
|
|
13
|
+
metadata: { ...(route.$$route ? route.$$route.require().route.metadata : {}), filesystem: true },
|
|
11
14
|
component: lazyRoute(
|
|
12
15
|
route.$component,
|
|
13
16
|
import.meta.env.START_ISLANDS
|
|
@@ -24,5 +27,5 @@ export function createRoutes() {
|
|
|
24
27
|
|
|
25
28
|
let routes;
|
|
26
29
|
export const FileRoutes = () => {
|
|
27
|
-
return isServer ? getRequestEvent().routes :
|
|
30
|
+
return isServer ? (getRequestEvent() as PageEvent).routes : routes || (routes = createRoutes());
|
|
28
31
|
};
|
package/shared/HttpHeader.tsx
CHANGED
|
@@ -3,27 +3,8 @@ import { getRequestEvent, isServer } from "solid-js/web";
|
|
|
3
3
|
|
|
4
4
|
export function HttpHeader(props: { name: string; value: string; append?: boolean }) {
|
|
5
5
|
if (isServer) {
|
|
6
|
-
const
|
|
7
|
-
if (props
|
|
8
|
-
pageContext!.appendResponseHeader(props.name, props.value);
|
|
9
|
-
} else {
|
|
10
|
-
pageContext!.setResponseHeader(props.name, props.value);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
onCleanup(() => {
|
|
14
|
-
const value = pageContext!.getResponseHeader(props.name);
|
|
15
|
-
|
|
16
|
-
// Todo: review this logic still the same in H3
|
|
17
|
-
// H3 supports arrays so its possible there is no need to split..
|
|
18
|
-
// typeof check here is to guard but ultimately may be unneeded
|
|
19
|
-
if (value && typeof value === "string") {
|
|
20
|
-
const values = value.split(", ");
|
|
21
|
-
const index = values.indexOf(props.value);
|
|
22
|
-
index !== -1 && values.splice(index, 1);
|
|
23
|
-
if (values.length) pageContext!.setResponseHeader(props.name, values.join(", "));
|
|
24
|
-
else pageContext!.removeResponseHeader(props.name);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
6
|
+
const event = getRequestEvent();
|
|
7
|
+
if (event) onCleanup((event as any).components.header(props));
|
|
27
8
|
}
|
|
28
9
|
|
|
29
10
|
return null;
|
|
@@ -3,11 +3,8 @@ import { getRequestEvent, isServer } from "solid-js/web";
|
|
|
3
3
|
|
|
4
4
|
export function HttpStatusCode(props: { code: number, text?: string }) {
|
|
5
5
|
if (isServer) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
onCleanup(() => {
|
|
9
|
-
context!.setResponseStatus(200);
|
|
10
|
-
});
|
|
6
|
+
const event = getRequestEvent();
|
|
7
|
+
if (event) onCleanup((event as any).components.status(props));
|
|
11
8
|
}
|
|
12
9
|
return null;
|
|
13
10
|
}
|
package/shared/index.tsx
CHANGED
package/shared/lazyRoute.tsx
CHANGED
|
@@ -47,9 +47,10 @@ export default function lazyRoute(
|
|
|
47
47
|
const mod = await component.import();
|
|
48
48
|
const Component = mod[exported];
|
|
49
49
|
let assets = await clientManifest.inputs?.[component.src].assets();
|
|
50
|
+
const styles = assets.filter((asset) => asset.tag === "style" || asset.attrs.rel === "stylesheet");
|
|
50
51
|
const Comp = (props) => {
|
|
51
52
|
return [
|
|
52
|
-
...
|
|
53
|
+
...styles.map((asset) => renderAsset(asset)),
|
|
53
54
|
createComponent(Component, props),
|
|
54
55
|
];
|
|
55
56
|
};
|
package/shared/routes.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
import fileRoutes
|
|
1
|
+
import fileRoutes from "vinxi/routes";
|
|
2
|
+
import { HTTPMethod } from "vinxi/server";
|
|
3
|
+
|
|
4
|
+
interface Route {
|
|
5
|
+
path: string;
|
|
6
|
+
id: string;
|
|
7
|
+
type: "api" | "page";
|
|
8
|
+
children?: Route[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type MatchRoute = Route & {
|
|
12
|
+
score: number;
|
|
13
|
+
params: {
|
|
14
|
+
type: "*" | ":";
|
|
15
|
+
name: string;
|
|
16
|
+
index: number;
|
|
17
|
+
}[];
|
|
18
|
+
matchSegments: (string | null)[];
|
|
19
|
+
wildcard: boolean;
|
|
20
|
+
};
|
|
2
21
|
|
|
3
22
|
declare module "vinxi/routes" {
|
|
4
23
|
export interface Register {
|
|
@@ -10,23 +29,65 @@ declare module "vinxi/routes" {
|
|
|
10
29
|
}
|
|
11
30
|
}
|
|
12
31
|
|
|
13
|
-
const
|
|
32
|
+
export const pageRoutes = defineRoutes((fileRoutes as unknown as Route[]).filter(o => o.type === "page"));
|
|
33
|
+
const apiRoutes = defineAPIRoutes((fileRoutes as unknown as Route[]).filter(o => o.type === "api"));
|
|
34
|
+
|
|
35
|
+
export function matchAPIRoute(path: string, method: HTTPMethod) {
|
|
36
|
+
const segments = path.split("/").filter(Boolean);
|
|
37
|
+
|
|
38
|
+
routeLoop: for (const route of apiRoutes) {
|
|
39
|
+
const matchSegments = route.matchSegments;
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
segments.length < matchSegments.length ||
|
|
43
|
+
(!route.wildcard && segments.length > matchSegments.length)
|
|
44
|
+
) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (let index = 0; index < matchSegments.length; index++) {
|
|
49
|
+
const match = matchSegments[index];
|
|
50
|
+
if (!match) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (segments[index] !== match) {
|
|
55
|
+
continue routeLoop;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handler = route[`$${method}`];
|
|
60
|
+
if (handler === "skip" || handler === undefined) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const params: Record<string, string> = {};
|
|
65
|
+
for (const { type, name, index } of route.params) {
|
|
66
|
+
if (type === ":") {
|
|
67
|
+
params[name] = segments[index];
|
|
68
|
+
} else {
|
|
69
|
+
params[name] = segments.slice(index).join("/");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { handler, params };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function defineRoutes(fileRoutes: Route[]) {
|
|
14
78
|
function processRoute(routes: Route[], route: Route, id: string, full: string) {
|
|
15
79
|
const parentRoute = Object.values(routes).find(o => {
|
|
16
|
-
|
|
17
|
-
// return false;
|
|
18
|
-
// }
|
|
19
|
-
return id.startsWith(o.path + "/");
|
|
80
|
+
return id.startsWith(o.id + "/");
|
|
20
81
|
});
|
|
21
82
|
|
|
22
83
|
if (!parentRoute) {
|
|
23
|
-
routes.push({ ...route, path: id });
|
|
84
|
+
routes.push({ ...route, id, path: id.replace(/\/\([^)/]+\)/g, "") });
|
|
24
85
|
return routes;
|
|
25
86
|
}
|
|
26
87
|
processRoute(
|
|
27
88
|
parentRoute.children || (parentRoute.children = []),
|
|
28
89
|
route,
|
|
29
|
-
id.slice(parentRoute.
|
|
90
|
+
id.slice(parentRoute.id.length),
|
|
30
91
|
full
|
|
31
92
|
);
|
|
32
93
|
|
|
@@ -38,7 +99,80 @@ const defineRoutes = (fileRoutes: Route[]) => {
|
|
|
38
99
|
.reduce((prevRoutes, route) => {
|
|
39
100
|
return processRoute(prevRoutes, route, route.path, route.path);
|
|
40
101
|
}, []);
|
|
41
|
-
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function defineAPIRoutes(routes: Route[]) {
|
|
105
|
+
return routes
|
|
106
|
+
.flatMap(route => {
|
|
107
|
+
const paths = expandOptionals(route.path);
|
|
108
|
+
return paths.map(path => ({ ...route, path }));
|
|
109
|
+
})
|
|
110
|
+
.map(routeToMatchRoute)
|
|
111
|
+
.sort((a, b) => b.score - a.score);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// This is copied from https://github.com/solidjs/solid-router/blob/main/src/utils.ts
|
|
115
|
+
function expandOptionals(pattern: string): string[] {
|
|
116
|
+
let match = /(\/?\:[^\/]+)\?/.exec(pattern);
|
|
117
|
+
if (!match) return [pattern];
|
|
118
|
+
|
|
119
|
+
let prefix = pattern.slice(0, match.index);
|
|
120
|
+
let suffix = pattern.slice(match.index + match[0].length);
|
|
121
|
+
const prefixes: string[] = [prefix, (prefix += match[1])];
|
|
122
|
+
|
|
123
|
+
// This section handles adjacent optional params. We don't actually want all permuations since
|
|
124
|
+
// that will lead to equivalent apiRwhich have the same number of params. For example
|
|
125
|
+
// `/:a?/:b?/:c`? only has the unique expansion: `/`, `/:a`, `/:a/:b`, `/:a/:b/:c` and we can
|
|
126
|
+
// discard `/:b`, `/:c`, `/:b/:c` by building them up in order and not recursing. This also helps
|
|
127
|
+
// ensure predictability where earlier params have precidence.
|
|
128
|
+
while ((match = /^(\/\:[^\/]+)\?/.exec(suffix))) {
|
|
129
|
+
prefixes.push((prefix += match[1]));
|
|
130
|
+
suffix = suffix.slice(match[0].length);
|
|
131
|
+
}
|
|
42
132
|
|
|
43
|
-
|
|
44
|
-
|
|
133
|
+
return expandOptionals(suffix).reduce<string[]>(
|
|
134
|
+
(results, expansion) => [...results, ...prefixes.map(p => p + expansion)],
|
|
135
|
+
[]
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function routeToMatchRoute(route: Route): MatchRoute {
|
|
140
|
+
const segments = route.path.split("/").filter(Boolean);
|
|
141
|
+
|
|
142
|
+
const params: { type: "*" | ":"; name: string; index: number }[] = [];
|
|
143
|
+
const matchSegments: (string | null)[] = [];
|
|
144
|
+
let score = 0;
|
|
145
|
+
let wildcard = false;
|
|
146
|
+
|
|
147
|
+
for (const [index, segment] of segments.entries()) {
|
|
148
|
+
if (segment[0] === ":") {
|
|
149
|
+
const name = segment.slice(1);
|
|
150
|
+
score += 3;
|
|
151
|
+
params.push({
|
|
152
|
+
type: ":",
|
|
153
|
+
name,
|
|
154
|
+
index
|
|
155
|
+
});
|
|
156
|
+
matchSegments.push(null);
|
|
157
|
+
} else if (segment[0] === "*") {
|
|
158
|
+
score -= 1;
|
|
159
|
+
params.push({
|
|
160
|
+
type: "*",
|
|
161
|
+
name: segment.slice(1),
|
|
162
|
+
index
|
|
163
|
+
});
|
|
164
|
+
wildcard = true;
|
|
165
|
+
} else {
|
|
166
|
+
score += 4;
|
|
167
|
+
matchSegments.push(segment);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
...route,
|
|
173
|
+
score,
|
|
174
|
+
params,
|
|
175
|
+
matchSegments,
|
|
176
|
+
wildcard
|
|
177
|
+
};
|
|
178
|
+
}
|