@solidjs/start 0.4.11 → 0.5.0
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/api-handler.ts +64 -0
- package/config/fs-router.js +39 -14
- package/config/index.d.ts +2 -1
- package/config/index.js +38 -13
- package/config/{server-fns-runtime.jsx → server-fns-runtime.ts} +2 -1
- package/config/{server-handler.js → server-handler.ts} +9 -17
- package/config/{server-runtime.jsx → server-runtime.ts} +6 -2
- package/{server/middleware.ts → middleware/index.ts} +12 -46
- package/package.json +10 -10
- package/server/StartServer.tsx +3 -3
- package/server/fetchEvent.ts +115 -0
- package/{entry/index.ts → server/handler.ts} +17 -19
- package/server/index.tsx +2 -2
- package/server/{page-event.tsx → pageEvent.ts} +5 -31
- 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 +1 -132
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
}
|
package/config/fs-router.js
CHANGED
|
@@ -57,19 +57,7 @@ export class SolidStartClientFileRouter extends BaseFileSystemRouter {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
function createHTTPHandlers(src, exports) {
|
|
62
|
-
const handlers = {};
|
|
63
|
-
for (const exp of exports) {
|
|
64
|
-
if (HTTP_METHODS.includes(exp.n)) {
|
|
65
|
-
handlers[`$${exp.n}`] = {
|
|
66
|
-
src: src,
|
|
67
|
-
pick: [exp.n]
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return handlers;
|
|
72
|
-
}
|
|
60
|
+
|
|
73
61
|
export class SolidStartServerFileRouter extends BaseFileSystemRouter {
|
|
74
62
|
toPath(src) {
|
|
75
63
|
const routePath = cleanPath(src, this.config)
|
|
@@ -123,7 +111,44 @@ export class SolidStartServerFileRouter extends BaseFileSystemRouter {
|
|
|
123
111
|
path,
|
|
124
112
|
filePath: src
|
|
125
113
|
};
|
|
126
|
-
}
|
|
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))) {
|
|
127
152
|
return {
|
|
128
153
|
type: "api",
|
|
129
154
|
...createHTTPHandlers(src, exports),
|
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,7 @@ 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 { SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.js";
|
|
11
|
+
import { SolidStartAPIFileRouter, SolidStartClientFileRouter, SolidStartServerFileRouter } from "./fs-router.js";
|
|
12
12
|
import { serverComponents } from "./server-components.js";
|
|
13
13
|
|
|
14
14
|
const DEFAULT_EXTENSIONS = ["js", "jsx", "ts", "tsx"];
|
|
@@ -18,7 +18,7 @@ function solidStartClientFsRouter(config) {
|
|
|
18
18
|
new SolidStartClientFileRouter(
|
|
19
19
|
{
|
|
20
20
|
dir: resolve.absolute(config.dir, router.root),
|
|
21
|
-
extensions: config.extensions ??
|
|
21
|
+
extensions: config.extensions ?? DEFAULT_EXTENSIONS
|
|
22
22
|
},
|
|
23
23
|
router,
|
|
24
24
|
app
|
|
@@ -30,7 +30,19 @@ function solidStartServerFsRouter(config) {
|
|
|
30
30
|
new SolidStartServerFileRouter(
|
|
31
31
|
{
|
|
32
32
|
dir: resolve.absolute(config.dir, router.root),
|
|
33
|
-
extensions: config.extensions ??
|
|
33
|
+
extensions: config.extensions ?? DEFAULT_EXTENSIONS
|
|
34
|
+
},
|
|
35
|
+
router,
|
|
36
|
+
app
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
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
|
|
34
46
|
},
|
|
35
47
|
router,
|
|
36
48
|
app
|
|
@@ -44,7 +56,12 @@ export function defineConfig(baseConfig = {}) {
|
|
|
44
56
|
appRoot: "./src",
|
|
45
57
|
ssr: true,
|
|
46
58
|
islands: false,
|
|
47
|
-
solid: {}
|
|
59
|
+
solid: {},
|
|
60
|
+
server: {
|
|
61
|
+
experimental: {
|
|
62
|
+
asyncContext: true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
48
65
|
});
|
|
49
66
|
let server = start.server;
|
|
50
67
|
if (!start.ssr) {
|
|
@@ -65,16 +82,24 @@ export function defineConfig(baseConfig = {}) {
|
|
|
65
82
|
routers: [
|
|
66
83
|
{
|
|
67
84
|
name: "public",
|
|
68
|
-
|
|
85
|
+
type: "static",
|
|
69
86
|
dir: "./public",
|
|
70
87
|
base: "/"
|
|
71
88
|
},
|
|
72
|
-
|
|
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
|
+
},
|
|
73
98
|
{
|
|
74
99
|
name: "ssr",
|
|
75
|
-
|
|
100
|
+
type: "http",
|
|
76
101
|
link: {
|
|
77
|
-
client: "client"
|
|
102
|
+
client: start.islands ? undefined : "client"
|
|
78
103
|
},
|
|
79
104
|
handler: `${start.appRoot}/entry-server${entryExtension}`,
|
|
80
105
|
middleware: start.middleware,
|
|
@@ -96,7 +121,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
96
121
|
...(typeof plugins === "function" ? [...(await plugins())] : plugins),
|
|
97
122
|
|
|
98
123
|
serverTransform({
|
|
99
|
-
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.
|
|
124
|
+
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url)))
|
|
100
125
|
}),
|
|
101
126
|
start.islands ? serverComponents.server() : null,
|
|
102
127
|
solid({ ...start.solid, ssr: true, extensions: extensions.map(ext => `.${ext}`) }),
|
|
@@ -125,7 +150,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
125
150
|
},
|
|
126
151
|
{
|
|
127
152
|
name: "client",
|
|
128
|
-
|
|
153
|
+
type: "client",
|
|
129
154
|
handler: `${start.appRoot}/entry-client${entryExtension}`,
|
|
130
155
|
...(start.islands
|
|
131
156
|
? {}
|
|
@@ -148,7 +173,7 @@ export function defineConfig(baseConfig = {}) {
|
|
|
148
173
|
}),
|
|
149
174
|
...(typeof plugins === "function" ? [...(await plugins())] : plugins),
|
|
150
175
|
serverFunctions.client({
|
|
151
|
-
runtime: normalize(fileURLToPath(new URL("./server-runtime.
|
|
176
|
+
runtime: normalize(fileURLToPath(new URL("./server-runtime.ts", import.meta.url)))
|
|
152
177
|
}),
|
|
153
178
|
start.islands ? serverComponents.client() : null,
|
|
154
179
|
solid({ ...start.solid, ssr: start.ssr, extensions: extensions.map(ext => `.${ext}`) }),
|
|
@@ -182,8 +207,8 @@ export function defineConfig(baseConfig = {}) {
|
|
|
182
207
|
base: "/_build"
|
|
183
208
|
},
|
|
184
209
|
serverFunctions.router({
|
|
185
|
-
handler: normalize(fileURLToPath(new URL("./server-handler.
|
|
186
|
-
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.
|
|
210
|
+
handler: normalize(fileURLToPath(new URL("./server-handler.ts", import.meta.url))),
|
|
211
|
+
runtime: normalize(fileURLToPath(new URL("./server-fns-runtime.ts", import.meta.url))),
|
|
187
212
|
// routes: solidStartServerFsRouter({ dir: `${start.appRoot}/routes`, extensions }),
|
|
188
213
|
plugins: async () => [
|
|
189
214
|
config("user", {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getRequestEvent } from "solid-js/web";
|
|
2
|
+
//@ts-ignore
|
|
2
3
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
3
|
-
import { cloneEvent } from "../server/
|
|
4
|
+
import { cloneEvent } from "../server/fetchEvent";
|
|
4
5
|
|
|
5
6
|
export function createServerReference(fn, id, name) {
|
|
6
7
|
if (typeof fn !== "function") throw new Error("Export from a 'use server' module must be a function");
|
|
@@ -17,7 +17,7 @@ import { sharedConfig } from "solid-js";
|
|
|
17
17
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
18
18
|
import invariant from "vinxi/lib/invariant";
|
|
19
19
|
import { eventHandler, setHeader } from "vinxi/server";
|
|
20
|
-
import { getFetchEvent } from "../server/
|
|
20
|
+
import { getFetchEvent, mergeResponseHeaders } from "../server/fetchEvent";
|
|
21
21
|
|
|
22
22
|
function createChunk(data) {
|
|
23
23
|
const bytes = data.length;
|
|
@@ -94,13 +94,13 @@ async function handleServerFunction(h3Event) {
|
|
|
94
94
|
) {
|
|
95
95
|
// workaround for https://github.com/unjs/nitro/issues/1721
|
|
96
96
|
// (issue only in edge runtimes)
|
|
97
|
-
parsed.push(await new Request(request, { ...request, body:
|
|
97
|
+
parsed.push(await new Request(request, { ...request, body: h3Event.node.req.body }).formData());
|
|
98
98
|
// what should work when #1721 is fixed
|
|
99
99
|
// parsed.push(await request.formData);
|
|
100
100
|
} else {
|
|
101
101
|
// workaround for https://github.com/unjs/nitro/issues/1721
|
|
102
102
|
// (issue only in edge runtimes)
|
|
103
|
-
const tmpReq = new Request(request, { ...request, body:
|
|
103
|
+
const tmpReq = new Request(request, { ...request, body: h3Event.node.req.body })
|
|
104
104
|
// what should work when #1721 is fixed
|
|
105
105
|
// just use request.json() here
|
|
106
106
|
parsed = fromJSON(await tmpReq.json(), {
|
|
@@ -137,13 +137,9 @@ async function handleServerFunction(h3Event) {
|
|
|
137
137
|
});
|
|
138
138
|
}
|
|
139
139
|
// forward headers
|
|
140
|
-
if (result.headers)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
if (result.customBody) {
|
|
146
|
-
result = await result.customBody();
|
|
140
|
+
if (result.headers) mergeResponseHeaders(h3Event, result.headers);
|
|
141
|
+
if ((result as any).customBody) {
|
|
142
|
+
result = await (result as any).customBody();
|
|
147
143
|
} else if (result.body == undefined) result = undefined;
|
|
148
144
|
}
|
|
149
145
|
|
|
@@ -182,13 +178,9 @@ async function handleServerFunction(h3Event) {
|
|
|
182
178
|
});
|
|
183
179
|
}
|
|
184
180
|
// forward headers
|
|
185
|
-
if (x.headers)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (x.customBody) {
|
|
191
|
-
x = await x.customBody();
|
|
181
|
+
if (x.headers) mergeResponseHeaders(h3Event, x.headers);
|
|
182
|
+
if ((x as any).customBody) {
|
|
183
|
+
x = await (x as any).customBody();
|
|
192
184
|
} else if (x.body == undefined) x = undefined;
|
|
193
185
|
}
|
|
194
186
|
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
|
-
defineMiddleware,
|
|
3
|
-
getRequestIP,
|
|
4
2
|
H3Event,
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
defineMiddleware,
|
|
4
|
+
sendWebResponse
|
|
7
5
|
} from "vinxi/server";
|
|
8
|
-
import {
|
|
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
|
-
}
|
|
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.0",
|
|
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"
|
|
@@ -44,15 +43,16 @@
|
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
46
45
|
"solid-js": "^1.8.12",
|
|
47
|
-
"vinxi": "^0.1
|
|
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,115 @@
|
|
|
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/server";
|
|
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
|
+
// @ts-ignore
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return h3Event[fetchEventSymbol];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function mergeResponseHeaders(h3Event, headers) {
|
|
50
|
+
for (const [key, value] of headers.entries()) {
|
|
51
|
+
setHeader(h3Event, key, value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class HeaderProxy {
|
|
56
|
+
constructor(private event: H3Event) {}
|
|
57
|
+
get(key: string) {
|
|
58
|
+
const h = getResponseHeader(this.event, key);
|
|
59
|
+
return Array.isArray(h) ? h.join(", ") : h;
|
|
60
|
+
}
|
|
61
|
+
has(key: string) {
|
|
62
|
+
return this.get(key) !== undefined;
|
|
63
|
+
}
|
|
64
|
+
set(key: string, value: string) {
|
|
65
|
+
return setResponseHeader(this.event, key, value);
|
|
66
|
+
}
|
|
67
|
+
delete(key: string) {
|
|
68
|
+
return removeResponseHeader(this.event, key);
|
|
69
|
+
}
|
|
70
|
+
append(key: string, value: string) {
|
|
71
|
+
appendResponseHeader(this.event, key, value);
|
|
72
|
+
}
|
|
73
|
+
getSetCookie() {
|
|
74
|
+
const cookies = getResponseHeader(this.event, "Set-Cookie");
|
|
75
|
+
return Array.isArray(cookies) ? cookies : [cookies];
|
|
76
|
+
}
|
|
77
|
+
forEach(fn: (value: string, key: string, object: Headers) => void) {
|
|
78
|
+
return this.entries().forEach(([key, value]) => fn(value, key, this));
|
|
79
|
+
}
|
|
80
|
+
entries() {
|
|
81
|
+
return getResponseHeaders(this.event).map(([key, value]) => [
|
|
82
|
+
key,
|
|
83
|
+
Array.isArray(value) ? value.join(", ") : value
|
|
84
|
+
]);
|
|
85
|
+
}
|
|
86
|
+
keys() {
|
|
87
|
+
return getResponseHeaders(this.event).map(([key]) => key);
|
|
88
|
+
}
|
|
89
|
+
values() {
|
|
90
|
+
return getResponseHeaders(this.event).map(([key, value]) =>
|
|
91
|
+
Array.isArray(value) ? value.join(", ") : value
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
[Symbol.iterator]() {
|
|
95
|
+
return this.entries()[Symbol.iterator]();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createResponseStub(event: H3Event): ResponseStub {
|
|
100
|
+
return {
|
|
101
|
+
get status() {
|
|
102
|
+
return getResponseStatus(event);
|
|
103
|
+
},
|
|
104
|
+
set status(v) {
|
|
105
|
+
setResponseStatus(event, v);
|
|
106
|
+
},
|
|
107
|
+
get statusText() {
|
|
108
|
+
return getResponseStatusText(event);
|
|
109
|
+
},
|
|
110
|
+
set statusText(v) {
|
|
111
|
+
setResponseStatus(event, getResponseStatus(), v);
|
|
112
|
+
},
|
|
113
|
+
headers: new HeaderProxy(event)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { sharedConfig } from "solid-js";
|
|
2
|
-
import { renderToStream } from "solid-js/web";
|
|
2
|
+
import { renderToStream, renderToString } from "solid-js/web";
|
|
3
3
|
/* @ts-ignore */
|
|
4
4
|
import { provideRequestEvent } from "solid-js/web/storage";
|
|
5
5
|
import {
|
|
6
6
|
EventHandlerObject,
|
|
7
7
|
EventHandlerRequest,
|
|
8
8
|
H3Event,
|
|
9
|
-
HTTPMethod,
|
|
10
9
|
eventHandler,
|
|
11
10
|
sendRedirect,
|
|
12
11
|
setHeader,
|
|
13
12
|
setResponseStatus
|
|
14
13
|
} from "vinxi/server";
|
|
15
|
-
import { getFetchEvent } from "
|
|
16
|
-
import { createPageEvent } from "
|
|
17
|
-
import {
|
|
18
|
-
import { matchAPIRoute } from "../shared/routes";
|
|
14
|
+
import { getFetchEvent } from "./fetchEvent";
|
|
15
|
+
import { createPageEvent } from "./pageEvent";
|
|
16
|
+
import { FetchEvent, PageEvent } from "./types";
|
|
19
17
|
|
|
20
18
|
export function createBaseHandler(
|
|
21
19
|
fn: (context: PageEvent) => unknown,
|
|
@@ -35,21 +33,21 @@ 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
|
-
//
|
|
41
|
-
const match = matchAPIRoute(new URL(event.request.url).pathname, event.request.method as HTTPMethod)
|
|
42
|
-
if (match) {
|
|
43
|
-
const mod = await match.handler.import();
|
|
44
|
-
const fn = mod[event.request.method];
|
|
45
|
-
(event as APIEvent).params = match.params;
|
|
46
|
-
sharedConfig.context = { event } as any;
|
|
47
|
-
return await fn(event);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// render stream
|
|
51
|
-
const doAsync = import.meta.env.START_SSR === "async";
|
|
39
|
+
// render
|
|
52
40
|
const context = await createPageEvent(event);
|
|
41
|
+
if (mode === "sync") {
|
|
42
|
+
const html = renderToString(() => {
|
|
43
|
+
(sharedConfig.context as any).event = context;
|
|
44
|
+
return fn(context);
|
|
45
|
+
}, options);
|
|
46
|
+
if (context.response && context.response.headers.get("Location")) {
|
|
47
|
+
return sendRedirect(event, context.response.headers.get("Location"));
|
|
48
|
+
}
|
|
49
|
+
return html;
|
|
50
|
+
}
|
|
53
51
|
let cloned = { ...options };
|
|
54
52
|
if (cloned.onCompleteAll) {
|
|
55
53
|
const og = cloned.onCompleteAll;
|
|
@@ -72,7 +70,7 @@ export function createBaseHandler(
|
|
|
72
70
|
if (context.response && context.response.headers.get("Location")) {
|
|
73
71
|
return sendRedirect(event, context.response.headers.get("Location"));
|
|
74
72
|
}
|
|
75
|
-
if (
|
|
73
|
+
if (mode === "async") return stream;
|
|
76
74
|
// fix cloudflare streaming
|
|
77
75
|
const { writable, readable } = new TransformStream();
|
|
78
76
|
stream.pipeTo(writable);
|
package/server/index.tsx
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
appendResponseHeader,
|
|
3
2
|
getCookie,
|
|
4
|
-
|
|
5
|
-
removeResponseHeader,
|
|
6
|
-
setCookie,
|
|
7
|
-
setResponseHeader,
|
|
8
|
-
setResponseStatus
|
|
3
|
+
setCookie
|
|
9
4
|
} from "vinxi/server";
|
|
10
5
|
import { createRoutes } from "../shared/FileRoutes";
|
|
11
6
|
import { FetchEvent, PageEvent } from "./types";
|
|
@@ -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/server";
|
|
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,24 +1,12 @@
|
|
|
1
1
|
import fileRoutes from "vinxi/routes";
|
|
2
|
-
import { HTTPMethod } from "vinxi/server";
|
|
3
2
|
|
|
4
3
|
interface Route {
|
|
5
4
|
path: string;
|
|
6
5
|
id: string;
|
|
7
|
-
type: "
|
|
6
|
+
type: "page";
|
|
8
7
|
children?: Route[];
|
|
9
8
|
}
|
|
10
9
|
|
|
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
10
|
declare module "vinxi/routes" {
|
|
23
11
|
export interface Register {
|
|
24
12
|
route: {
|
|
@@ -30,49 +18,6 @@ declare module "vinxi/routes" {
|
|
|
30
18
|
}
|
|
31
19
|
|
|
32
20
|
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.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
|
-
}
|
|
76
21
|
|
|
77
22
|
function defineRoutes(fileRoutes: Route[]) {
|
|
78
23
|
function processRoute(routes: Route[], route: Route, id: string, full: string) {
|
|
@@ -101,79 +46,3 @@ function defineRoutes(fileRoutes: Route[]) {
|
|
|
101
46
|
}, []);
|
|
102
47
|
}
|
|
103
48
|
|
|
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
|
-
}
|
|
132
|
-
|
|
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
|
-
const params: { type: "*" | ":"; name: string; index: number }[] = [];
|
|
142
|
-
const matchSegments: (string | null)[] = [];
|
|
143
|
-
let score = 0;
|
|
144
|
-
let wildcard = false;
|
|
145
|
-
|
|
146
|
-
for (const [index, segment] of segments.entries()) {
|
|
147
|
-
if (segment[0] === ":") {
|
|
148
|
-
const name = segment.slice(1);
|
|
149
|
-
score += 3;
|
|
150
|
-
params.push({
|
|
151
|
-
type: ":",
|
|
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
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
...route,
|
|
174
|
-
score,
|
|
175
|
-
params,
|
|
176
|
-
matchSegments,
|
|
177
|
-
wildcard
|
|
178
|
-
};
|
|
179
|
-
}
|