@solidjs/start 0.4.11 → 0.5.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/fs-router.js +11 -16
- package/config/index.d.ts +2 -1
- package/config/index.js +20 -13
- package/config/{server-fns-runtime.jsx → server-fns-runtime.ts} +1 -1
- package/config/{server-handler.js → server-handler.ts} +10 -19
- package/config/{server-runtime.jsx → server-runtime.ts} +6 -2
- package/{server/middleware.ts → middleware/index.ts} +14 -48
- package/package.json +11 -11
- package/server/StartServer.tsx +3 -3
- package/server/fetchEvent.ts +114 -0
- package/{entry/index.ts → server/handler.ts} +31 -16
- package/server/index.tsx +2 -2
- package/server/{page-event.tsx → pageEvent.ts} +7 -33
- package/{entry/spa/index.ts → server/spa/handler.ts} +2 -2
- package/server/spa/index.tsx +1 -2
- package/server/types.ts +14 -4
- package/shared/FileRoutes.tsx +2 -2
- package/shared/HttpHeader.tsx +18 -1
- package/shared/HttpStatusCode.tsx +3 -1
- package/shared/routes.ts +32 -129
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"]
|
|
@@ -57,6 +55,7 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
|
|
|
57
55
|
}
|
|
58
56
|
}
|
|
59
57
|
}
|
|
58
|
+
|
|
60
59
|
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
|
|
61
60
|
function createHTTPHandlers(src, exports) {
|
|
62
61
|
const handlers = {};
|
|
@@ -70,6 +69,7 @@ function createHTTPHandlers(src, exports) {
|
|
|
70
69
|
}
|
|
71
70
|
return handlers;
|
|
72
71
|
}
|
|
72
|
+
|
|
73
73
|
export class SolidStartServerFileRouter extends BaseFileSystemRouter {
|
|
74
74
|
toPath(src) {
|
|
75
75
|
const routePath = cleanPath(src, this.config)
|
|
@@ -91,10 +91,8 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
|
|
|
91
91
|
|
|
92
92
|
toRoute(src) {
|
|
93
93
|
let path = this.toPath(src);
|
|
94
|
-
|
|
95
94
|
if (src.endsWith(".md") || src.endsWith(".mdx")) {
|
|
96
95
|
return {
|
|
97
|
-
type: "page",
|
|
98
96
|
$component: {
|
|
99
97
|
src: src,
|
|
100
98
|
pick: ["$css"]
|
|
@@ -107,25 +105,22 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
|
|
|
107
105
|
|
|
108
106
|
const [_, exports] = analyzeModule(src);
|
|
109
107
|
const hasRouteConfig = exports.find(e => e.n === "route");
|
|
110
|
-
|
|
108
|
+
const hasDefault = exports.find(e => e.n === "default");
|
|
109
|
+
const hasAPIRoutes = exports.find(exp => HTTP_METHODS.includes(exp.n));
|
|
110
|
+
if (hasDefault || hasAPIRoutes) {
|
|
111
111
|
return {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
$component: hasDefault
|
|
113
|
+
? {
|
|
114
|
+
src: src,
|
|
115
|
+
pick: ["default", "$css"]
|
|
116
|
+
}
|
|
117
|
+
: undefined,
|
|
117
118
|
$$route: hasRouteConfig
|
|
118
119
|
? {
|
|
119
120
|
src: src,
|
|
120
121
|
pick: ["route"]
|
|
121
122
|
}
|
|
122
123
|
: undefined,
|
|
123
|
-
path,
|
|
124
|
-
filePath: src
|
|
125
|
-
};
|
|
126
|
-
} else if (exports.find(exp => HTTP_METHODS.includes(exp.n))) {
|
|
127
|
-
return {
|
|
128
|
-
type: "api",
|
|
129
124
|
...createHTTPHandlers(src, exports),
|
|
130
125
|
path,
|
|
131
126
|
filePath: src
|
package/config/index.d.ts
CHANGED
|
@@ -9,8 +9,9 @@ type SolidStartInlineConfig = Omit<InlineConfig, "router"> & {
|
|
|
9
9
|
* true: streaming mode
|
|
10
10
|
* false: csr only
|
|
11
11
|
* async: ssr is in async mode
|
|
12
|
+
* sync: ssr is in sync mode
|
|
12
13
|
*/
|
|
13
|
-
ssr?: boolean | "async",
|
|
14
|
+
ssr?: boolean | "async" | "sync",
|
|
14
15
|
solid?: Options,
|
|
15
16
|
extensions?: string[],
|
|
16
17
|
server?: AppOptions['server'],
|
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"];
|
|
@@ -18,7 +21,7 @@ function solidStartClientFsRouter(config) {
|
|
|
18
21
|
new SolidStartClientFileRouter(
|
|
19
22
|
{
|
|
20
23
|
dir: resolve.absolute(config.dir, router.root),
|
|
21
|
-
extensions: config.extensions ??
|
|
24
|
+
extensions: config.extensions ?? DEFAULT_EXTENSIONS
|
|
22
25
|
},
|
|
23
26
|
router,
|
|
24
27
|
app
|
|
@@ -30,7 +33,7 @@ function solidStartServerFsRouter(config) {
|
|
|
30
33
|
new SolidStartServerFileRouter(
|
|
31
34
|
{
|
|
32
35
|
dir: resolve.absolute(config.dir, router.root),
|
|
33
|
-
extensions: config.extensions ??
|
|
36
|
+
extensions: config.extensions ?? DEFAULT_EXTENSIONS
|
|
34
37
|
},
|
|
35
38
|
router,
|
|
36
39
|
app
|
|
@@ -44,7 +47,12 @@ export function defineConfig(baseConfig = {}) {
|
|
|
44
47
|
appRoot: "./src",
|
|
45
48
|
ssr: true,
|
|
46
49
|
islands: false,
|
|
47
|
-
solid: {}
|
|
50
|
+
solid: {},
|
|
51
|
+
server: {
|
|
52
|
+
experimental: {
|
|
53
|
+
asyncContext: true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
48
56
|
});
|
|
49
57
|
let server = start.server;
|
|
50
58
|
if (!start.ssr) {
|
|
@@ -65,16 +73,15 @@ export function defineConfig(baseConfig = {}) {
|
|
|
65
73
|
routers: [
|
|
66
74
|
{
|
|
67
75
|
name: "public",
|
|
68
|
-
|
|
76
|
+
type: "static",
|
|
69
77
|
dir: "./public",
|
|
70
78
|
base: "/"
|
|
71
79
|
},
|
|
72
|
-
|
|
73
80
|
{
|
|
74
81
|
name: "ssr",
|
|
75
|
-
|
|
82
|
+
type: "http",
|
|
76
83
|
link: {
|
|
77
|
-
client: "client"
|
|
84
|
+
client: start.islands ? undefined : "client"
|
|
78
85
|
},
|
|
79
86
|
handler: `${start.appRoot}/entry-server${entryExtension}`,
|
|
80
87
|
middleware: start.middleware,
|
|
@@ -96,7 +103,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
96
103
|
...(typeof plugins === "function" ? [...(await plugins())] : plugins),
|
|
97
104
|
|
|
98
105
|
serverTransform({
|
|
99
|
-
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.
|
|
106
|
+
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url)))
|
|
100
107
|
}),
|
|
101
108
|
start.islands ? serverComponents.server() : null,
|
|
102
109
|
solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
@@ -125,7 +132,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
125
132
|
},
|
|
126
133
|
{
|
|
127
134
|
name: "client",
|
|
128
|
-
|
|
135
|
+
type: "client",
|
|
129
136
|
handler: `${start.appRoot}/entry-client${entryExtension}`,
|
|
130
137
|
...(start.islands
|
|
131
138
|
? {}
|
|
@@ -148,7 +155,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
148
155
|
}),
|
|
149
156
|
...(typeof plugins === "function" ? [...(await plugins())] : plugins),
|
|
150
157
|
serverFunctions.client({
|
|
151
|
-
runtime: normalize(fileURLToPath(new URL("./server-runtime.
|
|
158
|
+
runtime: normalize(fileURLToPath(new URL("./server-runtime.ts", import.meta.url)))
|
|
152
159
|
}),
|
|
153
160
|
start.islands ? serverComponents.client() : null,
|
|
154
161
|
solid({ ...start.solid, ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
|
|
@@ -182,8 +189,8 @@ export function defineConfig(baseConfig = {}) {
|
|
|
182
189
|
base: "/_build"
|
|
183
190
|
},
|
|
184
191
|
serverFunctions.router({
|
|
185
|
-
handler: normalize(fileURLToPath(new URL("./server-handler.
|
|
186
|
-
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.
|
|
192
|
+
handler: normalize(fileURLToPath(new URL("./server-handler.ts", import.meta.url))),
|
|
193
|
+
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url))),
|
|
187
194
|
// routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
|
|
188
195
|
plugins: async () => [
|
|
189
196
|
config("user", {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getRequestEvent } from "solid-js/web";
|
|
2
2
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
3
|
-
import { cloneEvent } from "../server/
|
|
3
|
+
import { cloneEvent } from "../server/fetchEvent";
|
|
4
4
|
|
|
5
5
|
export function createServerReference(fn, id, name) {
|
|
6
6
|
if (typeof fn !== "function") throw new Error("Export from a 'use server' module must be a function");
|
|
@@ -13,11 +13,10 @@ import {
|
|
|
13
13
|
URLSearchParamsPlugin
|
|
14
14
|
} from "seroval-plugins/web";
|
|
15
15
|
import { sharedConfig } from "solid-js";
|
|
16
|
-
/* @ts-ignore */
|
|
17
16
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
17
|
+
import { eventHandler, setHeader } from "vinxi/http";
|
|
18
18
|
import invariant from "vinxi/lib/invariant";
|
|
19
|
-
import {
|
|
20
|
-
import { getFetchEvent } from "../server/middleware";
|
|
19
|
+
import { getFetchEvent, mergeResponseHeaders } from "../server/fetchEvent";
|
|
21
20
|
|
|
22
21
|
function createChunk(data) {
|
|
23
22
|
const bytes = data.length;
|
|
@@ -94,13 +93,13 @@ async function handleServerFunction(h3Event) {
|
|
|
94
93
|
) {
|
|
95
94
|
// workaround for https://github.com/unjs/nitro/issues/1721
|
|
96
95
|
// (issue only in edge runtimes)
|
|
97
|
-
parsed.push(await new Request(request, { ...request, body:
|
|
96
|
+
parsed.push(await new Request(request, { ...request, body: h3Event.node.req.body }).formData());
|
|
98
97
|
// what should work when #1721 is fixed
|
|
99
98
|
// parsed.push(await request.formData);
|
|
100
99
|
} else {
|
|
101
100
|
// workaround for https://github.com/unjs/nitro/issues/1721
|
|
102
101
|
// (issue only in edge runtimes)
|
|
103
|
-
const tmpReq = new Request(request, { ...request, body:
|
|
102
|
+
const tmpReq = new Request(request, { ...request, body: h3Event.node.req.body })
|
|
104
103
|
// what should work when #1721 is fixed
|
|
105
104
|
// just use request.json() here
|
|
106
105
|
parsed = fromJSON(await tmpReq.json(), {
|
|
@@ -137,13 +136,9 @@ async function handleServerFunction(h3Event) {
|
|
|
137
136
|
});
|
|
138
137
|
}
|
|
139
138
|
// forward headers
|
|
140
|
-
if (result.headers)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (result.customBody) {
|
|
146
|
-
result = await result.customBody();
|
|
139
|
+
if (result.headers) mergeResponseHeaders(h3Event, result.headers);
|
|
140
|
+
if ((result as any).customBody) {
|
|
141
|
+
result = await (result as any).customBody();
|
|
147
142
|
} else if (result.body == undefined) result = undefined;
|
|
148
143
|
}
|
|
149
144
|
|
|
@@ -182,13 +177,9 @@ async function handleServerFunction(h3Event) {
|
|
|
182
177
|
});
|
|
183
178
|
}
|
|
184
179
|
// forward headers
|
|
185
|
-
if (x.headers)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (x.customBody) {
|
|
191
|
-
x = await x.customBody();
|
|
180
|
+
if (x.headers) mergeResponseHeaders(h3Event, x.headers);
|
|
181
|
+
if ((x as any).customBody) {
|
|
182
|
+
x = await (x as any).customBody();
|
|
192
183
|
} else if (x.body == undefined) x = undefined;
|
|
193
184
|
}
|
|
194
185
|
return x;
|
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
import { createIslandReference } from "../server/islands";
|
|
15
15
|
|
|
16
16
|
class SerovalChunkReader {
|
|
17
|
+
reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
18
|
+
buffer: string;
|
|
19
|
+
done: boolean;
|
|
17
20
|
constructor(stream) {
|
|
18
21
|
this.reader = stream.getReader();
|
|
19
22
|
this.buffer = "";
|
|
@@ -92,6 +95,7 @@ async function deserializeStream(id, response) {
|
|
|
92
95
|
if (!result.done) {
|
|
93
96
|
reader.drain().then(
|
|
94
97
|
() => {
|
|
98
|
+
// @ts-ignore
|
|
95
99
|
delete $R[id];
|
|
96
100
|
},
|
|
97
101
|
() => {
|
|
@@ -105,7 +109,7 @@ async function deserializeStream(id, response) {
|
|
|
105
109
|
|
|
106
110
|
let INSTANCE = 0;
|
|
107
111
|
|
|
108
|
-
function createRequest(base, id, instance, body, contentType) {
|
|
112
|
+
function createRequest(base: string, id: string, instance: string, body: any, contentType?: string) {
|
|
109
113
|
return fetch(base, {
|
|
110
114
|
method: "POST",
|
|
111
115
|
headers: {
|
|
@@ -154,7 +158,7 @@ async function fetchServerFunction(base, id, method, args) {
|
|
|
154
158
|
|
|
155
159
|
if (response.headers.get("Location")) throw response;
|
|
156
160
|
if (response.headers.get("X-Revalidate")) {
|
|
157
|
-
/* ts-ignore-next-line */
|
|
161
|
+
/* @ts-ignore-next-line */
|
|
158
162
|
response.customBody = () => {
|
|
159
163
|
return deserializeStream(instance, response);
|
|
160
164
|
};
|
|
@@ -1,59 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "
|
|
8
|
-
import { FetchEvent } from "./types";
|
|
9
|
-
|
|
10
|
-
const h3EventSymbol = Symbol("h3Event");
|
|
11
|
-
const fetchEventSymbol = Symbol("fetchEvent");
|
|
12
|
-
const eventTraps = {
|
|
13
|
-
get(target, prop) {
|
|
14
|
-
if (prop === fetchEventSymbol) return target;
|
|
15
|
-
return target[prop] ?? target[h3EventSymbol][prop];
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export function createFetchEvent(event: H3Event): FetchEvent {
|
|
20
|
-
const request = toWebRequest(event);
|
|
21
|
-
return new Proxy(
|
|
22
|
-
{
|
|
23
|
-
request: request,
|
|
24
|
-
clientAddress: getRequestIP(event),
|
|
25
|
-
locals: {},
|
|
26
|
-
// @ts-ignore
|
|
27
|
-
[h3EventSymbol]: event
|
|
28
|
-
},
|
|
29
|
-
eventTraps
|
|
30
|
-
) as unknown as FetchEvent;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function cloneEvent<T extends FetchEvent>(fetchEvent: T): T {
|
|
34
|
-
return new Proxy({ ...fetchEvent[fetchEventSymbol] }, eventTraps);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function getFetchEvent(h3Event: H3Event): FetchEvent {
|
|
38
|
-
if (!h3Event[fetchEventSymbol]) {
|
|
39
|
-
const fetchEvent = createFetchEvent(h3Event);
|
|
40
|
-
h3Event[fetchEventSymbol] = fetchEvent;
|
|
41
|
-
// @ts-ignore
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return h3Event[fetchEventSymbol];
|
|
45
|
-
}
|
|
2
|
+
H3Event,
|
|
3
|
+
defineMiddleware,
|
|
4
|
+
sendWebResponse
|
|
5
|
+
} from "vinxi/http";
|
|
6
|
+
import { getFetchEvent } from "../server/fetchEvent";
|
|
7
|
+
import type { FetchEvent } from "../server/types";
|
|
46
8
|
|
|
47
9
|
/** Function responsible for receiving an observable [operation]{@link Operation} and returning a [result]{@link OperationResult}. */
|
|
48
10
|
|
|
49
11
|
export type MiddlewareFn = (event: FetchEvent) => Promise<unknown> | unknown;
|
|
50
12
|
/** This composes an array of Exchanges into a single ExchangeIO function */
|
|
51
13
|
|
|
52
|
-
type RequestMiddleware = (event: FetchEvent) => Response | Promise<Response> | void | Promise<void>;
|
|
14
|
+
export type RequestMiddleware = (event: FetchEvent) => Response | Promise<Response> | void | Promise<void>;
|
|
15
|
+
|
|
16
|
+
// copy-pasted from h3/dist/index.d.ts
|
|
17
|
+
type EventHandlerResponse<T = any> = T | Promise<T>;
|
|
18
|
+
type ResponseMiddlewareResponseParam = { body?: Awaited<EventHandlerResponse> };
|
|
53
19
|
|
|
54
|
-
type ResponseMiddleware = (
|
|
20
|
+
export type ResponseMiddleware = (
|
|
55
21
|
event: FetchEvent,
|
|
56
|
-
response:
|
|
22
|
+
response: ResponseMiddlewareResponseParam
|
|
57
23
|
) => Response | Promise<Response> | void | Promise<void>;
|
|
58
24
|
|
|
59
25
|
function wrapRequestMiddleware(onRequest: RequestMiddleware) {
|
|
@@ -69,7 +35,7 @@ function wrapRequestMiddleware(onRequest: RequestMiddleware) {
|
|
|
69
35
|
}
|
|
70
36
|
|
|
71
37
|
function wrapResponseMiddleware(onBeforeResponse: ResponseMiddleware) {
|
|
72
|
-
return async (h3Event: H3Event, response:
|
|
38
|
+
return async (h3Event: H3Event, response: ResponseMiddlewareResponseParam) => {
|
|
73
39
|
const fetchEvent = getFetchEvent(h3Event);
|
|
74
40
|
const mwResponse = await onBeforeResponse(fetchEvent, response);
|
|
75
41
|
if (!mwResponse) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidjs/start",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Ryan Carniato",
|
|
6
6
|
"publishConfig": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"client",
|
|
11
11
|
"config",
|
|
12
|
-
"
|
|
12
|
+
"middleware",
|
|
13
13
|
"server",
|
|
14
14
|
"shared",
|
|
15
15
|
"*.d.ts"
|
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./shared/index.tsx",
|
|
20
20
|
"./config": "./config/index.js",
|
|
21
|
-
"./
|
|
22
|
-
"./entry/spa": "./entry/spa/index.ts",
|
|
21
|
+
"./middleware": "./middleware/index.ts",
|
|
23
22
|
"./server": "./server/index.tsx",
|
|
24
23
|
"./server/spa": "./server/spa/index.tsx",
|
|
25
24
|
"./client": "./client/index.tsx",
|
|
@@ -31,8 +30,8 @@
|
|
|
31
30
|
".": [
|
|
32
31
|
"./shared/index.tsx"
|
|
33
32
|
],
|
|
34
|
-
"./
|
|
35
|
-
"./
|
|
33
|
+
"./middleware": [
|
|
34
|
+
"./middleware/index.ts"
|
|
36
35
|
],
|
|
37
36
|
"./server": [
|
|
38
37
|
"./server/index.tsx"
|
|
@@ -43,16 +42,17 @@
|
|
|
43
42
|
}
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
46
|
-
"solid-js": "^1.8.
|
|
47
|
-
"vinxi": "^0.1
|
|
45
|
+
"solid-js": "^1.8.13",
|
|
46
|
+
"vinxi": "^0.2.1"
|
|
48
47
|
},
|
|
49
48
|
"dependencies": {
|
|
50
|
-
"@vinxi/plugin-directives": "^0.
|
|
51
|
-
"@vinxi/server-components": "^0.
|
|
52
|
-
"@vinxi/server-functions": "^0.1
|
|
49
|
+
"@vinxi/plugin-directives": "^0.2.0",
|
|
50
|
+
"@vinxi/server-components": "^0.2.0",
|
|
51
|
+
"@vinxi/server-functions": "^0.2.1",
|
|
53
52
|
"defu": "^6.1.2",
|
|
54
53
|
"error-stack-parser": "^2.1.4",
|
|
55
54
|
"html-to-image": "^1.11.11",
|
|
55
|
+
"radix3": "^1.1.0",
|
|
56
56
|
"seroval": "^1.0.2",
|
|
57
57
|
"seroval-plugins": "^1.0.2",
|
|
58
58
|
"shikiji": "^0.9.12",
|
package/server/StartServer.tsx
CHANGED
|
@@ -35,9 +35,9 @@ export function StartServer(props: { document: Component<DocumentComponentProps>
|
|
|
35
35
|
|
|
36
36
|
let assets = [];
|
|
37
37
|
Promise.resolve().then(async () => {
|
|
38
|
-
if (context.
|
|
39
|
-
const matches = [...context.
|
|
40
|
-
while (matches.length && (!matches[0].
|
|
38
|
+
if (context.router && context.router.matches) {
|
|
39
|
+
const matches = [...context.router.matches];
|
|
40
|
+
while (matches.length && (!matches[0].info || !matches[0].info.filesystem))
|
|
41
41
|
matches.shift();
|
|
42
42
|
const matched = matches.length && matchRoute(matches, context.routes);
|
|
43
43
|
if (matched) {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
H3Event,
|
|
3
|
+
HTTPEventSymbol,
|
|
4
|
+
appendResponseHeader,
|
|
5
|
+
getRequestIP,
|
|
6
|
+
getResponseHeader,
|
|
7
|
+
getResponseHeaders,
|
|
8
|
+
getResponseStatus,
|
|
9
|
+
getResponseStatusText,
|
|
10
|
+
removeResponseHeader,
|
|
11
|
+
setHeader,
|
|
12
|
+
setResponseHeader,
|
|
13
|
+
setResponseStatus,
|
|
14
|
+
toWebRequest
|
|
15
|
+
} from "vinxi/http";
|
|
16
|
+
import type { FetchEvent, ResponseStub } from "./types";
|
|
17
|
+
|
|
18
|
+
const fetchEventSymbol = Symbol("fetchEvent");
|
|
19
|
+
|
|
20
|
+
export function createFetchEvent(event: H3Event): FetchEvent {
|
|
21
|
+
const request = toWebRequest(event);
|
|
22
|
+
return {
|
|
23
|
+
request: request,
|
|
24
|
+
response: createResponseStub(event),
|
|
25
|
+
clientAddress: getRequestIP(event),
|
|
26
|
+
locals: {},
|
|
27
|
+
nativeEvent: event,
|
|
28
|
+
[HTTPEventSymbol]: event
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function cloneEvent<T extends FetchEvent>(fetchEvent: T): T {
|
|
33
|
+
return {
|
|
34
|
+
...fetchEvent,
|
|
35
|
+
[HTTPEventSymbol]: fetchEvent[HTTPEventSymbol]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getFetchEvent(h3Event: H3Event): FetchEvent {
|
|
40
|
+
if (!h3Event[fetchEventSymbol]) {
|
|
41
|
+
const fetchEvent = createFetchEvent(h3Event);
|
|
42
|
+
h3Event[fetchEventSymbol] = fetchEvent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return h3Event[fetchEventSymbol];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function mergeResponseHeaders(h3Event, headers) {
|
|
49
|
+
for (const [key, value] of headers.entries()) {
|
|
50
|
+
setHeader(h3Event, key, value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class HeaderProxy {
|
|
55
|
+
constructor(private event: H3Event) {}
|
|
56
|
+
get(key: string) {
|
|
57
|
+
const h = getResponseHeader(this.event, key);
|
|
58
|
+
return Array.isArray(h) ? h.join(", ") : h;
|
|
59
|
+
}
|
|
60
|
+
has(key: string) {
|
|
61
|
+
return this.get(key) !== undefined;
|
|
62
|
+
}
|
|
63
|
+
set(key: string, value: string) {
|
|
64
|
+
return setResponseHeader(this.event, key, value);
|
|
65
|
+
}
|
|
66
|
+
delete(key: string) {
|
|
67
|
+
return removeResponseHeader(this.event, key);
|
|
68
|
+
}
|
|
69
|
+
append(key: string, value: string) {
|
|
70
|
+
appendResponseHeader(this.event, key, value);
|
|
71
|
+
}
|
|
72
|
+
getSetCookie() {
|
|
73
|
+
const cookies = getResponseHeader(this.event, "Set-Cookie");
|
|
74
|
+
return Array.isArray(cookies) ? cookies : [cookies];
|
|
75
|
+
}
|
|
76
|
+
forEach(fn: (value: string, key: string, object: Headers) => void) {
|
|
77
|
+
return this.entries().forEach(([key, value]) => fn(value, key, this));
|
|
78
|
+
}
|
|
79
|
+
entries() {
|
|
80
|
+
return getResponseHeaders(this.event).map(([key, value]) => [
|
|
81
|
+
key,
|
|
82
|
+
Array.isArray(value) ? value.join(", ") : value
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
keys() {
|
|
86
|
+
return getResponseHeaders(this.event).map(([key]) => key);
|
|
87
|
+
}
|
|
88
|
+
values() {
|
|
89
|
+
return getResponseHeaders(this.event).map(([key, value]) =>
|
|
90
|
+
Array.isArray(value) ? value.join(", ") : value
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
[Symbol.iterator]() {
|
|
94
|
+
return this.entries()[Symbol.iterator]();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createResponseStub(event: H3Event): ResponseStub {
|
|
99
|
+
return {
|
|
100
|
+
get status() {
|
|
101
|
+
return getResponseStatus(event);
|
|
102
|
+
},
|
|
103
|
+
set status(v) {
|
|
104
|
+
setResponseStatus(event, v);
|
|
105
|
+
},
|
|
106
|
+
get statusText() {
|
|
107
|
+
return getResponseStatusText(event);
|
|
108
|
+
},
|
|
109
|
+
set statusText(v) {
|
|
110
|
+
setResponseStatus(event, getResponseStatus(), v);
|
|
111
|
+
},
|
|
112
|
+
headers: new HeaderProxy(event)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { sharedConfig } from "solid-js";
|
|
2
|
-
import { renderToStream } from "solid-js/web";
|
|
3
|
-
/* @ts-ignore */
|
|
2
|
+
import { renderToStream, renderToString } from "solid-js/web";
|
|
4
3
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
5
4
|
import {
|
|
6
5
|
EventHandlerObject,
|
|
7
6
|
EventHandlerRequest,
|
|
8
7
|
H3Event,
|
|
9
|
-
HTTPMethod,
|
|
10
8
|
eventHandler,
|
|
11
9
|
sendRedirect,
|
|
12
10
|
setHeader,
|
|
13
11
|
setResponseStatus
|
|
14
|
-
} from "vinxi/
|
|
15
|
-
import { getFetchEvent } from "../server/middleware";
|
|
16
|
-
import { createPageEvent } from "../server/page-event";
|
|
17
|
-
import { APIEvent, FetchEvent, PageEvent } from "../server/types";
|
|
12
|
+
} from "vinxi/http";
|
|
18
13
|
import { matchAPIRoute } from "../shared/routes";
|
|
14
|
+
import { getFetchEvent } from "./fetchEvent";
|
|
15
|
+
import { createPageEvent } from "./pageEvent";
|
|
16
|
+
import type { APIEvent, FetchEvent, PageEvent } from "./types";
|
|
19
17
|
|
|
20
18
|
export function createBaseHandler(
|
|
21
19
|
fn: (context: PageEvent) => unknown,
|
|
@@ -35,21 +33,38 @@ export function createBaseHandler(
|
|
|
35
33
|
onBeforeResponse: options.onBeforeResponse,
|
|
36
34
|
handler: (e: H3Event<EventHandlerRequest> & { startEvent: FetchEvent }) => {
|
|
37
35
|
const event = getFetchEvent(e);
|
|
36
|
+
const mode = import.meta.env.START_SSR;
|
|
38
37
|
|
|
39
38
|
return provideRequestEvent(event, async () => {
|
|
40
39
|
// api
|
|
41
|
-
const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method
|
|
40
|
+
const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method);
|
|
42
41
|
if (match) {
|
|
43
42
|
const mod = await match.handler.import();
|
|
44
43
|
const fn = mod[event.request.method];
|
|
45
44
|
(event as APIEvent).params = match.params;
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
}
|
|
48
54
|
}
|
|
49
55
|
|
|
50
|
-
// render
|
|
51
|
-
const doAsync = import.meta.env.START_SSR === "async";
|
|
56
|
+
// render
|
|
52
57
|
const context = await createPageEvent(event);
|
|
58
|
+
if (mode === "sync") {
|
|
59
|
+
const html = renderToString(() => {
|
|
60
|
+
(sharedConfig.context as any).event = context;
|
|
61
|
+
return fn(context);
|
|
62
|
+
}, options);
|
|
63
|
+
if (context.response && context.response.headers.get("Location")) {
|
|
64
|
+
return sendRedirect(event, context.response.headers.get("Location"));
|
|
65
|
+
}
|
|
66
|
+
return html;
|
|
67
|
+
}
|
|
53
68
|
let cloned = { ...options };
|
|
54
69
|
if (cloned.onCompleteAll) {
|
|
55
70
|
const og = cloned.onCompleteAll;
|
|
@@ -67,12 +82,12 @@ export function createBaseHandler(
|
|
|
67
82
|
} else cloned.onCompleteShell = handleShellCompleteRedirect(context, e);
|
|
68
83
|
const stream = renderToStream(() => {
|
|
69
84
|
(sharedConfig.context as any).event = context;
|
|
70
|
-
return fn(context)
|
|
85
|
+
return fn(context);
|
|
71
86
|
}, cloned);
|
|
72
87
|
if (context.response && context.response.headers.get("Location")) {
|
|
73
88
|
return sendRedirect(event, context.response.headers.get("Location"));
|
|
74
89
|
}
|
|
75
|
-
if (
|
|
90
|
+
if (mode === "async") return stream;
|
|
76
91
|
// fix cloudflare streaming
|
|
77
92
|
const { writable, readable } = new TransformStream();
|
|
78
93
|
stream.pipeTo(writable);
|
|
@@ -88,7 +103,7 @@ function handleShellCompleteRedirect(context: PageEvent, e: H3Event<EventHandler
|
|
|
88
103
|
setResponseStatus(e, 302);
|
|
89
104
|
setHeader(e, "Location", context.response.headers.get("Location"));
|
|
90
105
|
}
|
|
91
|
-
}
|
|
106
|
+
};
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
function handleStreamCompleteRedirect(context: PageEvent) {
|
|
@@ -103,4 +118,4 @@ export function createHandler(
|
|
|
103
118
|
options: Parameters<typeof createBaseHandler>[1] = {}
|
|
104
119
|
) {
|
|
105
120
|
return createBaseHandler(fn, { ...options, createPageEvent });
|
|
106
|
-
}
|
|
121
|
+
}
|
package/server/index.tsx
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
removeResponseHeader,
|
|
6
|
-
setCookie,
|
|
7
|
-
setResponseHeader,
|
|
8
|
-
setResponseStatus
|
|
9
|
-
} from "vinxi/server";
|
|
2
|
+
getCookie,
|
|
3
|
+
setCookie
|
|
4
|
+
} from "vinxi/http";
|
|
10
5
|
import { createRoutes } from "../shared/FileRoutes";
|
|
11
6
|
import { FetchEvent, PageEvent } from "./types";
|
|
12
7
|
|
|
@@ -27,7 +22,7 @@ function initFromFlash(ctx: FetchEvent) {
|
|
|
27
22
|
export async function createPageEvent(ctx: FetchEvent) {
|
|
28
23
|
const clientManifest = import.meta.env.MANIFEST["client"];
|
|
29
24
|
const serverManifest = import.meta.env.MANIFEST["ssr"];
|
|
30
|
-
|
|
25
|
+
ctx.response.headers.set("Cache-Control", "no-cache");
|
|
31
26
|
// const prevPath = ctx.request.headers.get("x-solid-referrer");
|
|
32
27
|
// const mutation = ctx.request.headers.get("x-solid-mutation") === "true";
|
|
33
28
|
const pageEvent: PageEvent = Object.assign(ctx, {
|
|
@@ -40,31 +35,10 @@ export async function createPageEvent(ctx: FetchEvent) {
|
|
|
40
35
|
)
|
|
41
36
|
: [])
|
|
42
37
|
],
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
components: {
|
|
46
|
-
status: props => {
|
|
47
|
-
setResponseStatus(ctx, props.code, props.text);
|
|
48
|
-
return () => !ctx.handled && setResponseStatus(ctx, 200);
|
|
49
|
-
},
|
|
50
|
-
header: props => {
|
|
51
|
-
if (props.append) {
|
|
52
|
-
appendResponseHeader(ctx, props.name, props.value);
|
|
53
|
-
} else {
|
|
54
|
-
setResponseHeader(ctx, props.name, props.value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return () => {
|
|
58
|
-
if (ctx.handled) return;
|
|
59
|
-
let values = getResponseHeader(ctx, props.name);
|
|
60
|
-
if (!Array.isArray(values)) values = [values as string];
|
|
61
|
-
const index = values.indexOf(props.value);
|
|
62
|
-
index !== -1 && values.splice(index, 1);
|
|
63
|
-
if (values.length) setResponseHeader(ctx, props.name, values);
|
|
64
|
-
else removeResponseHeader(ctx, props.name);
|
|
65
|
-
};
|
|
66
|
-
}
|
|
38
|
+
router: {
|
|
39
|
+
submission: initFromFlash(ctx)
|
|
67
40
|
},
|
|
41
|
+
routes: createRoutes(),
|
|
68
42
|
// prevUrl: prevPath || "",
|
|
69
43
|
// mutation: mutation,
|
|
70
44
|
// $type: FETCH_EVENT,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createBaseHandler } from "
|
|
2
|
-
import { FetchEvent, PageEvent } from "
|
|
1
|
+
import { createBaseHandler } from "../handler";
|
|
2
|
+
import { FetchEvent, PageEvent } from "../types";
|
|
3
3
|
|
|
4
4
|
export function createHandler(
|
|
5
5
|
fn: (context: PageEvent) => unknown,
|
package/server/spa/index.tsx
CHANGED
package/server/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSX } from "solid-js";
|
|
2
|
-
import type
|
|
2
|
+
import { HTTPEventSymbol, type EventHandlerRequest, type H3Event } from "vinxi/http";
|
|
3
3
|
|
|
4
4
|
// export const FETCH_EVENT = "$FETCH";
|
|
5
5
|
|
|
@@ -16,10 +16,21 @@ export type ContextMatches = {
|
|
|
16
16
|
params: unknown;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
-
export interface
|
|
19
|
+
export interface ResponseStub {
|
|
20
|
+
status?: number;
|
|
21
|
+
statusText?: string;
|
|
22
|
+
headers: Headers;
|
|
23
|
+
}
|
|
24
|
+
export interface FetchEvent {
|
|
20
25
|
request: Request;
|
|
26
|
+
response: ResponseStub;
|
|
21
27
|
clientAddress: string;
|
|
22
|
-
locals:
|
|
28
|
+
locals: RequestEventLocals;
|
|
29
|
+
nativeEvent: H3Event<EventHandlerRequest>;
|
|
30
|
+
[HTTPEventSymbol]: H3Event<EventHandlerRequest>;
|
|
31
|
+
}
|
|
32
|
+
export interface RequestEventLocals {
|
|
33
|
+
[key: string | symbol]: any;
|
|
23
34
|
}
|
|
24
35
|
export interface PageEvent extends FetchEvent {
|
|
25
36
|
manifest: any;
|
|
@@ -29,7 +40,6 @@ export interface PageEvent extends FetchEvent {
|
|
|
29
40
|
// $type: typeof FETCH_EVENT;
|
|
30
41
|
$islands: Set<string>;
|
|
31
42
|
// mutation: boolean;
|
|
32
|
-
response?: Response;
|
|
33
43
|
}
|
|
34
44
|
|
|
35
45
|
export interface APIEvent extends FetchEvent {
|
package/shared/FileRoutes.tsx
CHANGED
|
@@ -10,8 +10,8 @@ export function createRoutes() {
|
|
|
10
10
|
return {
|
|
11
11
|
...route,
|
|
12
12
|
...(route.$$route ? route.$$route.require().route : undefined),
|
|
13
|
-
|
|
14
|
-
...(route.$$route ? route.$$route.require().route.
|
|
13
|
+
info: {
|
|
14
|
+
...(route.$$route ? route.$$route.require().route.info : {}),
|
|
15
15
|
filesystem: true
|
|
16
16
|
},
|
|
17
17
|
component: lazyRoute(
|
package/shared/HttpHeader.tsx
CHANGED
|
@@ -4,7 +4,24 @@ import { getRequestEvent, isServer } from "solid-js/web";
|
|
|
4
4
|
export function HttpHeader(props: { name: string; value: string; append?: boolean }) {
|
|
5
5
|
if (isServer) {
|
|
6
6
|
const event = getRequestEvent();
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
if (props.append) event.response.headers.append(props.name, props.value);
|
|
9
|
+
else event.response.headers.set(props.name, props.value);
|
|
10
|
+
|
|
11
|
+
onCleanup(() => {
|
|
12
|
+
if (event.nativeEvent.handled) return;
|
|
13
|
+
const value = event.response.headers.get(props.name);
|
|
14
|
+
if (!value) return;
|
|
15
|
+
if (!value.includes(", ")) {
|
|
16
|
+
if (value === props.value) event.response.headers.delete(props.name);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const values = value.split(", ");
|
|
20
|
+
const index = values.indexOf(props.value);
|
|
21
|
+
index !== -1 && values.splice(index, 1);
|
|
22
|
+
if (values.length) event.response.headers.set(props.name, values.join(","));
|
|
23
|
+
else event.response.headers.delete(props.name);
|
|
24
|
+
});
|
|
8
25
|
}
|
|
9
26
|
|
|
10
27
|
return null;
|
|
@@ -4,7 +4,9 @@ import { getRequestEvent, isServer } from "solid-js/web";
|
|
|
4
4
|
export function HttpStatusCode(props: { code: number, text?: string }) {
|
|
5
5
|
if (isServer) {
|
|
6
6
|
const event = getRequestEvent();
|
|
7
|
-
|
|
7
|
+
event.response.status = props.code;
|
|
8
|
+
event.response.statusText = props.text;
|
|
9
|
+
onCleanup(() => !event.nativeEvent.handled && (event.response.status = 200));
|
|
8
10
|
}
|
|
9
11
|
return null;
|
|
10
12
|
}
|
package/shared/routes.ts
CHANGED
|
@@ -1,78 +1,25 @@
|
|
|
1
|
+
import { createRouter } from "radix3";
|
|
1
2
|
import fileRoutes from "vinxi/routes";
|
|
2
|
-
import { HTTPMethod } from "vinxi/server";
|
|
3
3
|
|
|
4
4
|
interface Route {
|
|
5
5
|
path: string;
|
|
6
6
|
id: string;
|
|
7
|
-
type: "api" | "page";
|
|
8
7
|
children?: Route[];
|
|
8
|
+
$component?: any;
|
|
9
9
|
}
|
|
10
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
|
-
};
|
|
21
|
-
|
|
22
11
|
declare module "vinxi/routes" {
|
|
23
12
|
export interface Register {
|
|
24
13
|
route: {
|
|
25
14
|
path: string;
|
|
26
|
-
type: "api" | "page";
|
|
27
15
|
children?: Route[];
|
|
28
16
|
};
|
|
29
17
|
}
|
|
30
18
|
}
|
|
31
19
|
|
|
32
|
-
export const pageRoutes = defineRoutes(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
export function matchAPIRoute(path: string, method: HTTPMethod) {
|
|
36
|
-
const segments = path.replace(import.meta.env.SERVER_BASE_URL, "").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
|
-
}
|
|
20
|
+
export const pageRoutes = defineRoutes(
|
|
21
|
+
(fileRoutes as unknown as Route[]).filter(o => o.$component)
|
|
22
|
+
);
|
|
76
23
|
|
|
77
24
|
function defineRoutes(fileRoutes: Route[]) {
|
|
78
25
|
function processRoute(routes: Route[], route: Route, id: string, full: string) {
|
|
@@ -101,79 +48,35 @@ function defineRoutes(fileRoutes: Route[]) {
|
|
|
101
48
|
}, []);
|
|
102
49
|
}
|
|
103
50
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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);
|
|
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
|
+
};
|
|
131
60
|
}
|
|
132
|
-
|
|
133
|
-
return expandOptionals(suffix).reduce<string[]>(
|
|
134
|
-
(results, expansion) => [...results, ...prefixes.map(p => p + expansion)],
|
|
135
|
-
[]
|
|
136
|
-
);
|
|
137
61
|
}
|
|
138
62
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const matchSegments: (string | null)[] = [];
|
|
143
|
-
let score = 0;
|
|
144
|
-
let wildcard = false;
|
|
63
|
+
function containsHTTP(route: Route) {
|
|
64
|
+
return route["$GET"] || route["$POST"] || route["$PUT"] || route["$PATCH"] || route["$DELETE"];
|
|
65
|
+
}
|
|
145
66
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
name,
|
|
153
|
-
index
|
|
154
|
-
});
|
|
155
|
-
matchSegments.push(null);
|
|
156
|
-
} else if (segment[0] === "*") {
|
|
157
|
-
score -= 1;
|
|
158
|
-
params.push({
|
|
159
|
-
type: "*",
|
|
160
|
-
name: segment.slice(1),
|
|
161
|
-
index
|
|
162
|
-
});
|
|
163
|
-
wildcard = true;
|
|
164
|
-
} else {
|
|
165
|
-
score += 4;
|
|
166
|
-
if (!segment.match(/^\(.+\)$/)) {
|
|
167
|
-
matchSegments.push(segment);
|
|
168
|
-
}
|
|
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}`);
|
|
169
73
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
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
|
+
});
|