@solidjs/start 0.5.0 → 0.5.2
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/fs-router.js +23 -53
- package/config/index.js +5 -23
- package/config/server-fns-runtime.ts +1 -2
- package/config/server-handler.ts +67 -31
- package/config/server-runtime.ts +54 -32
- package/middleware/index.ts +4 -4
- package/package.json +2 -2
- package/server/fetchEvent.ts +7 -8
- package/server/handler.ts +23 -6
- package/server/pageEvent.ts +3 -3
- package/server/types.ts +1 -1
- package/shared/ErrorBoundary.tsx +3 -14
- package/shared/routes.ts +37 -3
- package/config/api-handler.ts +0 -64
package/config/fs-router.js
CHANGED
|
@@ -24,7 +24,6 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
|
|
|
24
24
|
|
|
25
25
|
if (src.endsWith(".md") || src.endsWith(".mdx")) {
|
|
26
26
|
return {
|
|
27
|
-
type: "page",
|
|
28
27
|
$component: {
|
|
29
28
|
src: src,
|
|
30
29
|
pick: ["$css"]
|
|
@@ -40,7 +39,6 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
|
|
|
40
39
|
const hasRouteConfig = exports.find(e => e.n === "route");
|
|
41
40
|
if (hasDefault) {
|
|
42
41
|
return {
|
|
43
|
-
type: "page",
|
|
44
42
|
$component: {
|
|
45
43
|
src: src,
|
|
46
44
|
pick: ["default", "$css"]
|
|
@@ -58,6 +56,20 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
|
|
|
58
56
|
}
|
|
59
57
|
}
|
|
60
58
|
|
|
59
|
+
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
|
|
60
|
+
function createHTTPHandlers(src, exports) {
|
|
61
|
+
const handlers = {};
|
|
62
|
+
for (const exp of exports) {
|
|
63
|
+
if (HTTP_METHODS.includes(exp.n)) {
|
|
64
|
+
handlers[`$${exp.n}`] = {
|
|
65
|
+
src: src,
|
|
66
|
+
pick: [exp.n]
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return handlers;
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
export class SolidStartServerFileRouter extends BaseFileSystemRouter {
|
|
62
74
|
toPath(src) {
|
|
63
75
|
const routePath = cleanPath(src, this.config)
|
|
@@ -79,10 +91,8 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
|
|
|
79
91
|
|
|
80
92
|
toRoute(src) {
|
|
81
93
|
let path = this.toPath(src);
|
|
82
|
-
|
|
83
94
|
if (src.endsWith(".md") || src.endsWith(".mdx")) {
|
|
84
95
|
return {
|
|
85
|
-
type: "page",
|
|
86
96
|
$component: {
|
|
87
97
|
src: src,
|
|
88
98
|
pick: ["$css"]
|
|
@@ -95,62 +105,22 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
|
|
|
95
105
|
|
|
96
106
|
const [_, exports] = analyzeModule(src);
|
|
97
107
|
const hasRouteConfig = exports.find(e => e.n === "route");
|
|
98
|
-
|
|
108
|
+
const hasDefault = exports.find(e => e.n === "default");
|
|
109
|
+
const hasAPIRoutes = exports.find(exp => HTTP_METHODS.includes(exp.n));
|
|
110
|
+
if (hasDefault || hasAPIRoutes) {
|
|
99
111
|
return {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
112
|
+
$component: hasDefault
|
|
113
|
+
? {
|
|
114
|
+
src: src,
|
|
115
|
+
pick: ["default", "$css"]
|
|
116
|
+
}
|
|
117
|
+
: undefined,
|
|
105
118
|
$$route: hasRouteConfig
|
|
106
119
|
? {
|
|
107
120
|
src: src,
|
|
108
121
|
pick: ["route"]
|
|
109
122
|
}
|
|
110
123
|
: undefined,
|
|
111
|
-
path,
|
|
112
|
-
filePath: src
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
|
|
119
|
-
function createHTTPHandlers(src, exports) {
|
|
120
|
-
const handlers = {};
|
|
121
|
-
for (const exp of exports) {
|
|
122
|
-
if (HTTP_METHODS.includes(exp.n)) {
|
|
123
|
-
handlers[`$${exp.n}`] = {
|
|
124
|
-
src: src,
|
|
125
|
-
pick: [exp.n]
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return handlers;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export class SolidStartAPIFileRouter extends BaseFileSystemRouter {
|
|
133
|
-
toPath(src) {
|
|
134
|
-
const routePath = cleanPath(src, this.config)
|
|
135
|
-
// remove the initial slash
|
|
136
|
-
.slice(1)
|
|
137
|
-
.replace(/index$/, "")
|
|
138
|
-
.replace(/\[([^\/]+)\]/g, (_, m) => {
|
|
139
|
-
if (m.length > 2 && m.startsWith("...")) {
|
|
140
|
-
return `**${m.length > 3 ? ":" + m.slice(3) : ""}`;
|
|
141
|
-
}
|
|
142
|
-
return `:${m}`;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
return routePath?.length > 0 ? `/${routePath}` : "/";
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
toRoute(src) {
|
|
149
|
-
let path = this.toPath(src);
|
|
150
|
-
const [_, exports] = analyzeModule(src);
|
|
151
|
-
if (exports.find(exp => HTTP_METHODS.includes(exp.n))) {
|
|
152
|
-
return {
|
|
153
|
-
type: "api",
|
|
154
124
|
...createHTTPHandlers(src, exports),
|
|
155
125
|
path,
|
|
156
126
|
filePath: src
|
package/config/index.js
CHANGED
|
@@ -8,7 +8,10 @@ import { createApp, resolve } from "vinxi";
|
|
|
8
8
|
import { normalize } from "vinxi/lib/path";
|
|
9
9
|
import { config } from "vinxi/plugins/config";
|
|
10
10
|
import solid from "vite-plugin-solid";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
SolidStartClientFileRouter,
|
|
13
|
+
SolidStartServerFileRouter
|
|
14
|
+
} from "./fs-router.js";
|
|
12
15
|
import { serverComponents } from "./server-components.js";
|
|
13
16
|
|
|
14
17
|
const DEFAULT_EXTENSIONS = ["js", "jsx", "ts", "tsx"];
|
|
@@ -37,18 +40,6 @@ function solidStartServerFsRouter(config) {
|
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
function solidStartAPIFileRouter(config) {
|
|
41
|
-
return (router, app) =>
|
|
42
|
-
new SolidStartAPIFileRouter(
|
|
43
|
-
{
|
|
44
|
-
dir: resolve.absolute(config.dir, router.root),
|
|
45
|
-
extensions: config.extensions ?? DEFAULT_EXTENSIONS
|
|
46
|
-
},
|
|
47
|
-
router,
|
|
48
|
-
app
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
43
|
export function defineConfig(baseConfig = {}) {
|
|
53
44
|
let { plugins = [], start = {}, ...userConfig } = baseConfig;
|
|
54
45
|
const extensions = [...DEFAULT_EXTENSIONS, ...(start.extensions || [])];
|
|
@@ -86,15 +77,6 @@ export function defineConfig(baseConfig = {}) {
|
|
|
86
77
|
dir: "./public",
|
|
87
78
|
base: "/"
|
|
88
79
|
},
|
|
89
|
-
{
|
|
90
|
-
name: "api",
|
|
91
|
-
type: "http",
|
|
92
|
-
target: "server",
|
|
93
|
-
base: "/",
|
|
94
|
-
handler: normalize(fileURLToPath(new URL("./api-handler.ts", import.meta.url))),
|
|
95
|
-
routes: solidStartAPIFileRouter({ dir: `${start.appRoot}/routes` }),
|
|
96
|
-
middleware: start.middleware
|
|
97
|
-
},
|
|
98
80
|
{
|
|
99
81
|
name: "ssr",
|
|
100
82
|
type: "http",
|
|
@@ -209,7 +191,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
209
191
|
serverFunctions.router({
|
|
210
192
|
handler: normalize(fileURLToPath(new URL("./server-handler.ts", import.meta.url))),
|
|
211
193
|
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url))),
|
|
212
|
-
|
|
194
|
+
routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
|
|
213
195
|
plugins: async () => [
|
|
214
196
|
config("user", {
|
|
215
197
|
...userConfig,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getRequestEvent } from "solid-js/web";
|
|
2
|
-
//@ts-ignore
|
|
3
2
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
4
3
|
import { cloneEvent } from "../server/fetchEvent";
|
|
5
4
|
|
|
@@ -9,7 +8,7 @@ export function createServerReference(fn, id, name) {
|
|
|
9
8
|
return new Proxy(fn, {
|
|
10
9
|
get(target, prop, receiver) {
|
|
11
10
|
if (prop === "url") {
|
|
12
|
-
return `${baseURL}/_server
|
|
11
|
+
return `${baseURL}/_server/?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`;
|
|
13
12
|
}
|
|
14
13
|
if (prop === "GET") return receiver;
|
|
15
14
|
},
|
package/config/server-handler.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="vinxi/types/server" />
|
|
2
2
|
import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval";
|
|
3
|
+
// @ts-ignore
|
|
3
4
|
import {
|
|
4
5
|
CustomEventPlugin,
|
|
5
6
|
DOMExceptionPlugin,
|
|
@@ -13,11 +14,15 @@ import {
|
|
|
13
14
|
URLSearchParamsPlugin
|
|
14
15
|
} from "seroval-plugins/web";
|
|
15
16
|
import { sharedConfig } from "solid-js";
|
|
16
|
-
|
|
17
|
+
import { renderToStringAsync } from "solid-js/web";
|
|
17
18
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
19
|
+
import { eventHandler, setHeader, setResponseStatus } from "vinxi/http";
|
|
18
20
|
import invariant from "vinxi/lib/invariant";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
+
import { cloneEvent, getFetchEvent, mergeResponseHeaders } from "../server/fetchEvent";
|
|
22
|
+
import { createPageEvent } from "../server/pageEvent";
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
import App from "#start/app";
|
|
25
|
+
import { FetchEvent, PageEvent } from "../server";
|
|
21
26
|
|
|
22
27
|
function createChunk(data) {
|
|
23
28
|
const bytes = data.length;
|
|
@@ -63,8 +68,9 @@ async function handleServerFunction(h3Event) {
|
|
|
63
68
|
const event = getFetchEvent(h3Event);
|
|
64
69
|
const request = event.request;
|
|
65
70
|
|
|
66
|
-
const serverReference = request.headers.get("
|
|
67
|
-
const instance = request.headers.get("
|
|
71
|
+
const serverReference = request.headers.get("X-Server-Id");
|
|
72
|
+
const instance = request.headers.get("X-Server-Instance");
|
|
73
|
+
const singleFlight = request.headers.has("X-Single-Flight");
|
|
68
74
|
const url = new URL(request.url);
|
|
69
75
|
let filepath, name;
|
|
70
76
|
if (serverReference) {
|
|
@@ -76,7 +82,7 @@ async function handleServerFunction(h3Event) {
|
|
|
76
82
|
if (!filepath || !name) throw new Error("Invalid request");
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
const
|
|
85
|
+
const serverFunction = (
|
|
80
86
|
await import.meta.env.MANIFEST[import.meta.env.ROUTER_NAME].chunks[filepath].import()
|
|
81
87
|
)[name];
|
|
82
88
|
let parsed = [];
|
|
@@ -94,13 +100,15 @@ async function handleServerFunction(h3Event) {
|
|
|
94
100
|
) {
|
|
95
101
|
// workaround for https://github.com/unjs/nitro/issues/1721
|
|
96
102
|
// (issue only in edge runtimes)
|
|
97
|
-
parsed.push(
|
|
98
|
-
|
|
103
|
+
parsed.push(
|
|
104
|
+
await new Request(request, { ...request, body: h3Event.node.req.body }).formData()
|
|
105
|
+
);
|
|
106
|
+
// what should work when #1721 is fixed
|
|
99
107
|
// parsed.push(await request.formData);
|
|
100
108
|
} else {
|
|
101
109
|
// workaround for https://github.com/unjs/nitro/issues/1721
|
|
102
110
|
// (issue only in edge runtimes)
|
|
103
|
-
const tmpReq = new Request(request, { ...request, body: h3Event.node.req.body })
|
|
111
|
+
const tmpReq = new Request(request, { ...request, body: h3Event.node.req.body });
|
|
104
112
|
// what should work when #1721 is fixed
|
|
105
113
|
// just use request.json() here
|
|
106
114
|
parsed = fromJSON(await tmpReq.json(), {
|
|
@@ -120,27 +128,23 @@ async function handleServerFunction(h3Event) {
|
|
|
120
128
|
}
|
|
121
129
|
}
|
|
122
130
|
try {
|
|
123
|
-
let result = await provideRequestEvent(event, () => {
|
|
131
|
+
let result = await provideRequestEvent(event, async () => {
|
|
124
132
|
/* @ts-ignore */
|
|
125
133
|
sharedConfig.context = { event };
|
|
126
|
-
return
|
|
134
|
+
return serverFunction(...parsed);
|
|
127
135
|
});
|
|
128
136
|
|
|
137
|
+
if (singleFlight && instance) {
|
|
138
|
+
result = await handleSingleFlight(event, result);
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
// handle responses
|
|
130
|
-
if (result instanceof Response) {
|
|
131
|
-
if (result.status === 302) {
|
|
132
|
-
return new Response(null, {
|
|
133
|
-
status: instance ? 204 : 302,
|
|
134
|
-
headers: {
|
|
135
|
-
Location: result.headers.get("Location")
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
}
|
|
142
|
+
if (result instanceof Response && instance) {
|
|
139
143
|
// forward headers
|
|
140
144
|
if (result.headers) mergeResponseHeaders(h3Event, result.headers);
|
|
141
145
|
if ((result as any).customBody) {
|
|
142
146
|
result = await (result as any).customBody();
|
|
143
|
-
} else if (result.body == undefined) result =
|
|
147
|
+
} else if (result.body == undefined) result = null;
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
// handle no JS success case
|
|
@@ -164,27 +168,59 @@ async function handleServerFunction(h3Event) {
|
|
|
164
168
|
}
|
|
165
169
|
});
|
|
166
170
|
}
|
|
167
|
-
if (typeof result === "string") return
|
|
171
|
+
if (!result || typeof result === "string") return result || null;
|
|
168
172
|
setHeader(h3Event, "content-type", "text/javascript");
|
|
169
173
|
return serializeToStream(instance, result);
|
|
170
174
|
} catch (x) {
|
|
171
175
|
if (x instanceof Response) {
|
|
172
|
-
if (x.status === 302)
|
|
173
|
-
return new Response(null, {
|
|
174
|
-
status: instance ? 204 : 302,
|
|
175
|
-
headers: {
|
|
176
|
-
Location: x.headers.get("Location")
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
}
|
|
176
|
+
if (x.status === 302 && !instance) setResponseStatus(h3Event, 302);
|
|
180
177
|
// forward headers
|
|
181
178
|
if (x.headers) mergeResponseHeaders(h3Event, x.headers);
|
|
182
179
|
if ((x as any).customBody) {
|
|
183
180
|
x = await (x as any).customBody();
|
|
184
|
-
} else if (x.body == undefined) x =
|
|
181
|
+
} else if (x.body == undefined) x = null;
|
|
185
182
|
}
|
|
186
183
|
return x;
|
|
187
184
|
}
|
|
188
185
|
}
|
|
189
186
|
|
|
187
|
+
async function handleSingleFlight(sourceEvent: FetchEvent, result: any) {
|
|
188
|
+
let revalidate;
|
|
189
|
+
let url = new URL(sourceEvent.request.headers.get("referer")).toString();
|
|
190
|
+
if (result instanceof Response) {
|
|
191
|
+
if (result.headers.has("X-Revalidate"))
|
|
192
|
+
revalidate = result.headers.get("X-Revalidate").split(",");
|
|
193
|
+
if (result.headers.has("Location")) url = result.headers.get("Location");
|
|
194
|
+
}
|
|
195
|
+
const event = cloneEvent(sourceEvent) as PageEvent;
|
|
196
|
+
event.request = new Request(url);
|
|
197
|
+
return await provideRequestEvent(event, async () => {
|
|
198
|
+
await createPageEvent(event);
|
|
199
|
+
/* @ts-ignore */
|
|
200
|
+
sharedConfig.context = { event };
|
|
201
|
+
/* @ts-ignore */
|
|
202
|
+
event.router.dataOnly = revalidate || true;
|
|
203
|
+
/* @ts-ignore */
|
|
204
|
+
event.router.previousUrl = sourceEvent.request.headers.get("referer");
|
|
205
|
+
renderToStringAsync(App);
|
|
206
|
+
/* @ts-ignore */
|
|
207
|
+
const body = event.router.data;
|
|
208
|
+
if (body) {
|
|
209
|
+
let containsKey = false;
|
|
210
|
+
for (const key in body) {
|
|
211
|
+
if (body[key] === undefined) delete body[key];
|
|
212
|
+
else containsKey = true;
|
|
213
|
+
}
|
|
214
|
+
if (!containsKey) return result;
|
|
215
|
+
if (!(result instanceof Response)) {
|
|
216
|
+
body["_$value"] = result;
|
|
217
|
+
result = new Response(null, { status: 200 });
|
|
218
|
+
}
|
|
219
|
+
result.customBody = () => body;
|
|
220
|
+
result.headers.set("X-Single-Flight", "true");
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
190
226
|
export default eventHandler(handleServerFunction);
|
package/config/server-runtime.ts
CHANGED
|
@@ -109,33 +109,32 @@ async function deserializeStream(id, response) {
|
|
|
109
109
|
|
|
110
110
|
let INSTANCE = 0;
|
|
111
111
|
|
|
112
|
-
function createRequest(base: string, id: string, instance: string,
|
|
112
|
+
function createRequest(base: string, id: string, instance: string, options: RequestInit) {
|
|
113
113
|
return fetch(base, {
|
|
114
114
|
method: "POST",
|
|
115
|
+
...options,
|
|
115
116
|
headers: {
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
body
|
|
117
|
+
...options.headers,
|
|
118
|
+
"X-Server-Id": id,
|
|
119
|
+
"X-Server-Instance": instance
|
|
120
|
+
}
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
async function fetchServerFunction(
|
|
124
|
+
async function fetchServerFunction(
|
|
125
|
+
base: string,
|
|
126
|
+
id: string,
|
|
127
|
+
options: Omit<RequestInit, "body">,
|
|
128
|
+
args: any[]
|
|
129
|
+
) {
|
|
125
130
|
const instance = `server-fn:${INSTANCE++}`;
|
|
126
|
-
const response = await (
|
|
127
|
-
?
|
|
128
|
-
headers: {
|
|
129
|
-
"x-server-instance": instance
|
|
130
|
-
}
|
|
131
|
-
})
|
|
131
|
+
const response = await (args.length === 0
|
|
132
|
+
? createRequest(base, id, instance, options)
|
|
132
133
|
: args.length === 1 && args[0] instanceof FormData
|
|
133
|
-
? createRequest(base, id, instance, args[0])
|
|
134
|
-
: createRequest(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
instance,
|
|
138
|
-
JSON.stringify(
|
|
134
|
+
? createRequest(base, id, instance, { ...options, body: args[0] })
|
|
135
|
+
: createRequest(base, id, instance, {
|
|
136
|
+
...options,
|
|
137
|
+
body: JSON.stringify(
|
|
139
138
|
await Promise.resolve(
|
|
140
139
|
toJSONAsync(args, {
|
|
141
140
|
plugins: [
|
|
@@ -153,17 +152,23 @@ async function fetchServerFunction(base, id, method, args) {
|
|
|
153
152
|
})
|
|
154
153
|
)
|
|
155
154
|
),
|
|
156
|
-
"application/json"
|
|
157
|
-
));
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
response.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
155
|
+
headers: { ...options.headers, "Content-Type": "application/json" }
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
response.headers.get("Location") ||
|
|
160
|
+
response.headers.get("X-Revalidate") ||
|
|
161
|
+
response.headers.get("X-Single-Flight")
|
|
162
|
+
) {
|
|
163
|
+
if (response.body) {
|
|
164
|
+
/* @ts-ignore-next-line */
|
|
165
|
+
response.customBody = () => {
|
|
166
|
+
return deserializeStream(instance, response);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return response;
|
|
166
170
|
}
|
|
171
|
+
|
|
167
172
|
const contentType = response.headers.get("Content-Type");
|
|
168
173
|
let result;
|
|
169
174
|
if (contentType && contentType.startsWith("text/plain")) {
|
|
@@ -184,14 +189,31 @@ export function createServerReference(fn, id, name) {
|
|
|
184
189
|
return new Proxy(fn, {
|
|
185
190
|
get(target, prop, receiver) {
|
|
186
191
|
if (prop === "url") {
|
|
187
|
-
return `${baseURL}/_server
|
|
192
|
+
return `${baseURL}/_server/?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`;
|
|
188
193
|
}
|
|
189
194
|
if (prop === "GET") {
|
|
190
|
-
return (
|
|
195
|
+
return receiver.withOptions({ method: "GET" });
|
|
196
|
+
}
|
|
197
|
+
if (prop === "withOptions") {
|
|
198
|
+
return (options: RequestInit) => {
|
|
199
|
+
const fn = (...args) => {
|
|
200
|
+
const encodeArgs = options.method && options.method.toUpperCase() === "GET";
|
|
201
|
+
return fetchServerFunction(
|
|
202
|
+
encodeArgs
|
|
203
|
+
? receiver.url + (args.length ? `&args=${JSON.stringify(args)}` : "")
|
|
204
|
+
: `${baseURL}/_server`,
|
|
205
|
+
`${id}#${name}`,
|
|
206
|
+
options,
|
|
207
|
+
encodeArgs ? [] : args
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
fn.url = receiver.url;
|
|
211
|
+
return fn;
|
|
212
|
+
};
|
|
191
213
|
}
|
|
192
214
|
},
|
|
193
215
|
apply(target, thisArg, args) {
|
|
194
|
-
return fetchServerFunction(`${baseURL}/_server`, `${id}#${name}`,
|
|
216
|
+
return fetchServerFunction(`${baseURL}/_server`, `${id}#${name}`, {}, args);
|
|
195
217
|
}
|
|
196
218
|
});
|
|
197
219
|
}
|
package/middleware/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "vinxi/
|
|
2
|
+
H3Event,
|
|
3
|
+
defineMiddleware,
|
|
4
|
+
sendWebResponse
|
|
5
|
+
} from "vinxi/http";
|
|
6
6
|
import { getFetchEvent } from "../server/fetchEvent";
|
|
7
7
|
import type { FetchEvent } from "../server/types";
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidjs/start",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Ryan Carniato",
|
|
6
6
|
"publishConfig": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"solid-js": "^1.8.
|
|
45
|
+
"solid-js": "^1.8.14",
|
|
46
46
|
"vinxi": "^0.2.1"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
package/server/fetchEvent.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
setResponseHeader,
|
|
13
13
|
setResponseStatus,
|
|
14
14
|
toWebRequest
|
|
15
|
-
} from "vinxi/
|
|
15
|
+
} from "vinxi/http";
|
|
16
16
|
import type { FetchEvent, ResponseStub } from "./types";
|
|
17
17
|
|
|
18
18
|
const fetchEventSymbol = Symbol("fetchEvent");
|
|
@@ -40,7 +40,6 @@ export function getFetchEvent(h3Event: H3Event): FetchEvent {
|
|
|
40
40
|
if (!h3Event[fetchEventSymbol]) {
|
|
41
41
|
const fetchEvent = createFetchEvent(h3Event);
|
|
42
42
|
h3Event[fetchEventSymbol] = fetchEvent;
|
|
43
|
-
// @ts-ignore
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
return h3Event[fetchEventSymbol];
|
|
@@ -75,21 +74,21 @@ class HeaderProxy {
|
|
|
75
74
|
return Array.isArray(cookies) ? cookies : [cookies];
|
|
76
75
|
}
|
|
77
76
|
forEach(fn: (value: string, key: string, object: Headers) => void) {
|
|
78
|
-
return
|
|
77
|
+
return Object.entries<string | string[]>(getResponseHeaders(this.event)).forEach(([key, value]) => fn(Array.isArray(value) ? value.join(", ") : value, key, this));
|
|
79
78
|
}
|
|
80
79
|
entries() {
|
|
81
|
-
return getResponseHeaders(this.event).map(([key, value]) => [
|
|
80
|
+
return Object.entries<string | string[]>(getResponseHeaders(this.event)).map(([key, value]) => [
|
|
82
81
|
key,
|
|
83
82
|
Array.isArray(value) ? value.join(", ") : value
|
|
84
|
-
]);
|
|
83
|
+
] as [string, string])[Symbol.iterator]();
|
|
85
84
|
}
|
|
86
85
|
keys() {
|
|
87
|
-
return getResponseHeaders(this.event).
|
|
86
|
+
return Object.keys(getResponseHeaders(this.event))[Symbol.iterator]();
|
|
88
87
|
}
|
|
89
88
|
values() {
|
|
90
|
-
return getResponseHeaders(this.event).map((
|
|
89
|
+
return Object.values<string | string[]>(getResponseHeaders(this.event)).map((value) =>
|
|
91
90
|
Array.isArray(value) ? value.join(", ") : value
|
|
92
|
-
);
|
|
91
|
+
)[Symbol.iterator]();
|
|
93
92
|
}
|
|
94
93
|
[Symbol.iterator]() {
|
|
95
94
|
return this.entries()[Symbol.iterator]();
|
package/server/handler.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { sharedConfig } from "solid-js";
|
|
2
2
|
import { renderToStream, renderToString } from "solid-js/web";
|
|
3
|
-
/* @ts-ignore */
|
|
4
3
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
5
4
|
import {
|
|
6
5
|
EventHandlerObject,
|
|
@@ -10,10 +9,11 @@ import {
|
|
|
10
9
|
sendRedirect,
|
|
11
10
|
setHeader,
|
|
12
11
|
setResponseStatus
|
|
13
|
-
} from "vinxi/
|
|
12
|
+
} from "vinxi/http";
|
|
13
|
+
import { matchAPIRoute } from "../shared/routes";
|
|
14
14
|
import { getFetchEvent } from "./fetchEvent";
|
|
15
15
|
import { createPageEvent } from "./pageEvent";
|
|
16
|
-
import { FetchEvent, PageEvent } from "./types";
|
|
16
|
+
import type { APIEvent, FetchEvent, PageEvent } from "./types";
|
|
17
17
|
|
|
18
18
|
export function createBaseHandler(
|
|
19
19
|
fn: (context: PageEvent) => unknown,
|
|
@@ -36,6 +36,23 @@ export function createBaseHandler(
|
|
|
36
36
|
const mode = import.meta.env.START_SSR;
|
|
37
37
|
|
|
38
38
|
return provideRequestEvent(event, async () => {
|
|
39
|
+
// api
|
|
40
|
+
const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method);
|
|
41
|
+
if (match) {
|
|
42
|
+
const mod = await match.handler.import();
|
|
43
|
+
const fn = mod[event.request.method];
|
|
44
|
+
(event as APIEvent).params = match.params;
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
sharedConfig.context = { event };
|
|
47
|
+
const res = await fn(event);
|
|
48
|
+
if (res !== undefined) return res;
|
|
49
|
+
if (event.request.method !== "GET") {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`API handler for ${event.request.method} "${event.request.url}" did not return a response.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
// render
|
|
40
57
|
const context = await createPageEvent(event);
|
|
41
58
|
if (mode === "sync") {
|
|
@@ -65,7 +82,7 @@ export function createBaseHandler(
|
|
|
65
82
|
} else cloned.onCompleteShell = handleShellCompleteRedirect(context, e);
|
|
66
83
|
const stream = renderToStream(() => {
|
|
67
84
|
(sharedConfig.context as any).event = context;
|
|
68
|
-
return fn(context)
|
|
85
|
+
return fn(context);
|
|
69
86
|
}, cloned);
|
|
70
87
|
if (context.response && context.response.headers.get("Location")) {
|
|
71
88
|
return sendRedirect(event, context.response.headers.get("Location"));
|
|
@@ -86,7 +103,7 @@ function handleShellCompleteRedirect(context: PageEvent, e: H3Event<EventHandler
|
|
|
86
103
|
setResponseStatus(e, 302);
|
|
87
104
|
setHeader(e, "Location", context.response.headers.get("Location"));
|
|
88
105
|
}
|
|
89
|
-
}
|
|
106
|
+
};
|
|
90
107
|
}
|
|
91
108
|
|
|
92
109
|
function handleStreamCompleteRedirect(context: PageEvent) {
|
|
@@ -101,4 +118,4 @@ export function createHandler(
|
|
|
101
118
|
options: Parameters<typeof createBaseHandler>[1] = {}
|
|
102
119
|
) {
|
|
103
120
|
return createBaseHandler(fn, { ...options, createPageEvent });
|
|
104
|
-
}
|
|
121
|
+
}
|
package/server/pageEvent.ts
CHANGED
package/server/types.ts
CHANGED
package/shared/ErrorBoundary.tsx
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ErrorBoundary as DefaultErrorBoundary,
|
|
3
|
-
ParentProps,
|
|
4
|
-
} from "solid-js";
|
|
1
|
+
import { ErrorBoundary as DefaultErrorBoundary, type ParentProps } from "solid-js";
|
|
5
2
|
import { HttpStatusCode } from "./HttpStatusCode";
|
|
6
3
|
import { DevOverlay } from "./dev-overlay";
|
|
7
4
|
|
|
8
5
|
export function ErrorBoundary(props: ParentProps) {
|
|
9
6
|
if (import.meta.env.DEV) {
|
|
10
|
-
return
|
|
11
|
-
<DevOverlay>
|
|
12
|
-
{props.children}
|
|
13
|
-
</DevOverlay>
|
|
14
|
-
);
|
|
7
|
+
return <DevOverlay>{props.children}</DevOverlay>;
|
|
15
8
|
}
|
|
16
9
|
return (
|
|
17
|
-
<DefaultErrorBoundary
|
|
18
|
-
fallback={(
|
|
19
|
-
<HttpStatusCode code={500} />
|
|
20
|
-
)}
|
|
21
|
-
>
|
|
10
|
+
<DefaultErrorBoundary fallback={<HttpStatusCode code={500} />}>
|
|
22
11
|
{props.children}
|
|
23
12
|
</DefaultErrorBoundary>
|
|
24
13
|
);
|
package/shared/routes.ts
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
|
+
import { createRouter } from "radix3";
|
|
1
2
|
import fileRoutes from "vinxi/routes";
|
|
2
3
|
|
|
3
4
|
interface Route {
|
|
4
5
|
path: string;
|
|
5
6
|
id: string;
|
|
6
|
-
type: "page";
|
|
7
7
|
children?: Route[];
|
|
8
|
+
$component?: any;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
declare module "vinxi/routes" {
|
|
11
12
|
export interface Register {
|
|
12
13
|
route: {
|
|
13
14
|
path: string;
|
|
14
|
-
type: "api" | "page";
|
|
15
15
|
children?: Route[];
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export const pageRoutes = defineRoutes(
|
|
20
|
+
export const pageRoutes = defineRoutes(
|
|
21
|
+
(fileRoutes as unknown as Route[]).filter(o => o.$component)
|
|
22
|
+
);
|
|
21
23
|
|
|
22
24
|
function defineRoutes(fileRoutes: Route[]) {
|
|
23
25
|
function processRoute(routes: Route[], route: Route, id: string, full: string) {
|
|
@@ -46,3 +48,35 @@ function defineRoutes(fileRoutes: Route[]) {
|
|
|
46
48
|
}, []);
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
export function matchAPIRoute(path: string, method: string) {
|
|
52
|
+
const match = router.lookup(path);
|
|
53
|
+
if (match) {
|
|
54
|
+
const handler = match.route[`$${method}`];
|
|
55
|
+
if (handler === undefined) return;
|
|
56
|
+
return {
|
|
57
|
+
handler,
|
|
58
|
+
params: match.params
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function containsHTTP(route: Route) {
|
|
64
|
+
return route["$GET"] || route["$POST"] || route["$PUT"] || route["$PATCH"] || route["$DELETE"];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const router = createRouter({
|
|
68
|
+
routes: (fileRoutes as unknown as Route[]).reduce((memo, route) => {
|
|
69
|
+
if (!containsHTTP(route)) return memo;
|
|
70
|
+
let path = route.path.replace(/\(.*\)\/?/g, "").replace(/\*([^/]*)/g, (_, m) => `**:${m}`);
|
|
71
|
+
if (/:[^/]*\?/g.test(path)) {
|
|
72
|
+
throw new Error(`Optional parameters are not supported in API routes: ${path}`);
|
|
73
|
+
}
|
|
74
|
+
if (memo[path]) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Duplicate API routes for "${path}" found at "${memo[path].route.path}" and "${route.path}"`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
memo[path] = { route };
|
|
80
|
+
return memo;
|
|
81
|
+
}, {} as Record<string, { route: Route }>)
|
|
82
|
+
});
|
package/config/api-handler.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/// <reference types="vinxi/types/server" />
|
|
2
|
-
import { createRouter } from "radix3";
|
|
3
|
-
import { sharedConfig } from "solid-js";
|
|
4
|
-
import fileRoutes from "vinxi/routes";
|
|
5
|
-
|
|
6
|
-
// @ts-ignore
|
|
7
|
-
import { provideRequestEvent } from "solid-js/web/storage";
|
|
8
|
-
import { eventHandler } from "vinxi/server";
|
|
9
|
-
import { getFetchEvent } from "../server/fetchEvent";
|
|
10
|
-
import { APIEvent } from "../server/types";
|
|
11
|
-
|
|
12
|
-
export default eventHandler(h3Event => {
|
|
13
|
-
const event = getFetchEvent(h3Event);
|
|
14
|
-
const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method);
|
|
15
|
-
if (match) {
|
|
16
|
-
return provideRequestEvent(event, async () => {
|
|
17
|
-
const mod = await match.handler.import();
|
|
18
|
-
const fn = mod[event.request.method];
|
|
19
|
-
(event as APIEvent).params = match.params;
|
|
20
|
-
// @ts-ignore
|
|
21
|
-
sharedConfig.context = { event };
|
|
22
|
-
const res = await fn(event);
|
|
23
|
-
if (res === undefined && event.request.method !== "GET") {
|
|
24
|
-
throw new Error(
|
|
25
|
-
`API handler for ${event.request.method} "${event.request.url}" did not return a response.`
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return res;
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
interface Route {
|
|
34
|
-
path: string;
|
|
35
|
-
id: string;
|
|
36
|
-
type: "api" | "page";
|
|
37
|
-
children?: Route[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const router = createRouter({
|
|
41
|
-
routes: (fileRoutes as unknown as Route[]).reduce((memo, route) => {
|
|
42
|
-
if (route.type !== "api") return memo;
|
|
43
|
-
const path = route.path.replace(/\(.*\)\/?/g, "");
|
|
44
|
-
if (memo[path]) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`Duplicate API routes for "${path}" found at "${memo[path].route.path}" and "${route.path}"`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
memo[path] = { route };
|
|
50
|
-
return memo;
|
|
51
|
-
}, {} as Record<string, { route: Route }>)
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
function matchAPIRoute(path: string, method: string) {
|
|
55
|
-
const match = router.lookup(path);
|
|
56
|
-
if (match) {
|
|
57
|
-
const handler = match.route[`$${method}`];
|
|
58
|
-
if (handler === "skip" || handler === undefined) return;
|
|
59
|
-
return {
|
|
60
|
-
handler,
|
|
61
|
-
params: match.params
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|